leanback: new transport controls

PlaybackTransportRowPresenter is the new UX.

PlaybackTransportControlGlue extends from PlaybackGlue.
PlaybackTransportControlGlue is backed by PlayerWrapper.
PlaybackWrapper wraps underlying media player,
concrete example: MediaPlayerWrapper.

PlaybackSeekDataProvider defines the data interface that app
provides to leanback.
PlaybackUI defines seeking interaction between
PlaybackTransportControlGlue, PlaybackGlueHost and the presenter.

more:
- added progress bar support for media loading.
- fixed vertical video proportion and support video size change.

Test: PlaybackTransportControlGlueTest
PlaybackTransportRowPresenterTest.
Updated SampleVideoFragment.

Bug: 33751556

Change-Id: I1d39c6f65d04121f8cb9c25fb7ce103e32355190
diff --git a/v17/leanback/res/layout/lb_playback_fragment.xml b/v17/leanback/res/layout/lb_playback_fragment.xml
index 1b0ffa1..ab2909b 100644
--- a/v17/leanback/res/layout/lb_playback_fragment.xml
+++ b/v17/leanback/res/layout/lb_playback_fragment.xml
@@ -21,7 +21,13 @@
         android:transitionGroup="false"
         android:layout_height="match_parent">
 
-    <FrameLayout
+    <android.support.v17.leanback.widget.NonOverlappingFrameLayout
+        android:id="@+id/playback_fragment_background"
+        android:transitionGroup="false"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+    <android.support.v17.leanback.widget.NonOverlappingFrameLayout
             android:id="@+id/playback_controls_dock"
             android:transitionGroup="true"
             android:layout_height="match_parent"
diff --git a/v17/leanback/res/layout/lb_playback_transport_controls.xml b/v17/leanback/res/layout/lb_playback_transport_controls.xml
new file mode 100644
index 0000000..d4380f3
--- /dev/null
+++ b/v17/leanback/res/layout/lb_playback_transport_controls.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2014 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical" >
+
+    <FrameLayout
+        android:id="@+id/controls_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" >
+
+        <android.support.v17.leanback.widget.ControlBar
+            android:id="@+id/control_bar"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layoutDirection="ltr"
+            android:orientation="horizontal" />
+
+
+    </FrameLayout>
+
+
+</LinearLayout>
\ No newline at end of file
diff --git a/v17/leanback/res/layout/lb_playback_transport_controls_row.xml b/v17/leanback/res/layout/lb_playback_transport_controls_row.xml
new file mode 100644
index 0000000..1b32be6
--- /dev/null
+++ b/v17/leanback/res/layout/lb_playback_transport_controls_row.xml
@@ -0,0 +1,127 @@
+<?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.
+  -->
+
+<!-- Note: clipChildren/clipToPadding false are needed to apply shadows to child
+     views with no padding of their own. Also to allow for negative margin on description. -->
+
+<android.support.v17.leanback.widget.PlaybackTransportRowView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:clipChildren="false"
+    android:clipToPadding="false"
+    android:paddingBottom="@dimen/lb_playback_transport_control_row_padding_bottom"
+    android:paddingStart="?attr/browsePaddingStart"
+    android:paddingEnd="?attr/browsePaddingEnd" >
+
+    <FrameLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+        <LinearLayout
+            android:id="@+id/controls_card"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:clipChildren="false"
+            android:clipToPadding="false"
+            android:layout_marginBottom="@dimen/lb_playback_transport_control_info_margin_bottom"
+            android:orientation="horizontal" >
+
+            <ImageView
+                android:id="@+id/image"
+                android:layout_width="wrap_content"
+                android:layout_height="@dimen/lb_playback_transport_image_height"
+                android:layout_gravity="top"
+                android:adjustViewBounds="true"
+                android:layout_marginEnd="@dimen/lb_playback_transport_image_margin_end"
+                android:scaleType="fitStart" />
+
+            <FrameLayout
+                android:id="@+id/description_dock"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="top"
+                android:clipToPadding="false"
+                android:gravity="bottom" />
+        </LinearLayout>
+        <android.support.v17.leanback.widget.ThumbsBar
+            android:id="@+id/thumbs_row"
+            android:orientation="horizontal"
+            android:visibility="invisible"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="bottom" />
+    </FrameLayout>
+
+    <FrameLayout
+        android:id="@+id/controls_dock"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layoutDirection="ltr"
+        android:layout_marginLeft="@dimen/lb_playback_transport_controlbar_margin_start"
+    />
+
+    <android.support.v17.leanback.widget.SeekBar
+        android:id="@+id/playback_progress"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/lb_playback_transport_progressbar_height"
+        android:focusable="true" />
+
+    <RelativeLayout android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layoutDirection="ltr"
+                    android:layout_marginLeft="@dimen/lb_playback_transport_controlbar_margin_start">
+        <FrameLayout
+            android:id="@+id/secondary_controls_dock"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentStart="true" >
+        </FrameLayout>
+
+        <TextView
+            android:id="@+id/current_time"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="top"
+            android:layout_toStartOf="@+id/separate_time"
+            android:layout_marginStart="@dimen/lb_playback_transport_time_margin"
+            android:layout_marginTop="@dimen/lb_playback_transport_time_margin_top"
+            style="?attr/playbackControlsTimeStyle" />
+
+        <TextView
+            android:id="@+id/separate_time"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/lb_playback_time_separator"
+            android:layout_gravity="top"
+            android:layout_toStartOf="@+id/total_time"
+            android:layout_marginStart="@dimen/lb_playback_transport_time_margin"
+            android:layout_marginTop="@dimen/lb_playback_transport_time_margin_top"
+            style="?attr/playbackControlsTimeStyle" />
+
+        <TextView
+            android:id="@+id/total_time"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="top"
+            android:layout_alignParentEnd="true"
+            android:layout_marginStart="@dimen/lb_playback_transport_time_margin"
+            android:layout_marginTop="@dimen/lb_playback_transport_time_margin_top"
+            style="?attr/playbackControlsTimeStyle" />
+    </RelativeLayout>
+
+</android.support.v17.leanback.widget.PlaybackTransportRowView>
diff --git a/v17/leanback/res/layout/lb_video_surface.xml b/v17/leanback/res/layout/lb_video_surface.xml
index 9c6c8fd..61ac944 100644
--- a/v17/leanback/res/layout/lb_video_surface.xml
+++ b/v17/leanback/res/layout/lb_video_surface.xml
@@ -17,5 +17,6 @@
 <android.support.v17.leanback.widget.VideoSurfaceView
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/video_surface"
+    android:layout_gravity="center"
     android:layout_width="match_parent"
     android:layout_height="match_parent" />
diff --git a/v17/leanback/res/values/dimens.xml b/v17/leanback/res/values/dimens.xml
index 4bf237d..1f4299f 100644
--- a/v17/leanback/res/values/dimens.xml
+++ b/v17/leanback/res/values/dimens.xml
@@ -160,6 +160,25 @@
     <dimen name="lb_playback_now_playing_view_size">28dp</dimen>
     <dimen name="lb_playback_play_icon_size">14dp</dimen>
 
+    <!-- margin to move controlBar button a bit left to align icon with description left -->
+    <dimen name="lb_playback_transport_controlbar_margin_start">-12dp</dimen>
+    <dimen name="lb_playback_transport_control_info_margin_bottom">20dp</dimen>
+    <dimen name="lb_playback_transport_control_row_padding_bottom">20dp</dimen>
+    <dimen name="lb_playback_transport_image_height">176dp</dimen>
+    <dimen name="lb_playback_transport_image_margin_end">24dp</dimen>
+
+    <!-- height should including enough space for thumbs when activated -->
+    <dimen name="lb_playback_transport_progressbar_height">28dp</dimen>
+    <!-- height for the bar when not focused -->
+    <dimen name="lb_playback_transport_progressbar_bar_height">4dp</dimen>
+    <!-- height for the bar when focused -->
+    <dimen name="lb_playback_transport_progressbar_active_bar_height">6dp</dimen>
+    <!-- radius of thumb when focused -->
+    <dimen name="lb_playback_transport_progressbar_active_radius">6dp</dimen>
+
+    <dimen name="lb_playback_transport_time_margin">8dp</dimen>
+    <dimen name="lb_playback_transport_time_margin_top">8dp</dimen>
+
     <dimen name="lb_control_button_diameter">90dp</dimen>
     <dimen name="lb_control_button_height">64dp</dimen>
     <dimen name="lb_control_button_secondary_diameter">48dp</dimen>
diff --git a/v17/leanback/res/values/strings.xml b/v17/leanback/res/values/strings.xml
index cbf4904..aef086a 100644
--- a/v17/leanback/res/values/strings.xml
+++ b/v17/leanback/res/values/strings.xml
@@ -80,7 +80,8 @@
     <string name="lb_playback_controls_closed_captioning_disable">Disable Closed Captioning</string>
     <!-- Talkback label for the control button to enter picture in picture mode -->
     <string name="lb_playback_controls_picture_in_picture">Enter Picture In Picture Mode</string>
-
+    <!-- separator between current time and duration -->
+    <string name="lb_playback_time_separator">/</string>
 
     <string name="lb_playback_controls_shown">Media controls shown</string>
     <string name="lb_playback_controls_hidden">Media controls hidden, press d-pad to show</string>
@@ -95,6 +96,9 @@
     <!-- Separator for time picker [CHAR LIMIT=2] -->
     <string name="lb_time_separator">:</string>
 
+    <!-- Error string for MediaPlayer -->
+    <string name="lb_media_player_error">MediaPlayer error code %1$d extra %2$d</string>
+
     <!-- Onboarding screen -->
     <eat-comment />
     <!-- Text for "GET STARTED" button. This text should be in ALL CAPS. -->
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 5bae3d0..48bf74c 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java
@@ -125,10 +125,10 @@
         switch (mCurrentState) {
             case PLAY_VIDEO:
                 if (mPlaybackGlue != null) {
-                    if (mPlaybackGlue.isReadyForPlayback()) {
+                    if (mPlaybackGlue.isPrepared()) {
                         internalStartPlayback();
                     } else {
-                        mPlaybackGlue.setPlayerCallback(mControlStateCallback);
+                        mPlaybackGlue.addPlayerCallback(mControlStateCallback);
                     }
                 } else {
                     crossFadeBackgroundToVideo(false);
@@ -137,7 +137,7 @@
             case NO_VIDEO:
                 crossFadeBackgroundToVideo(false);
                 if (mPlaybackGlue != null) {
-                    mPlaybackGlue.setPlayerCallback(null);
+                    mPlaybackGlue.removePlayerCallback(mControlStateCallback);
                     mPlaybackGlue.pause();
                 }
                 break;
@@ -146,7 +146,7 @@
 
     void setPlaybackGlue(PlaybackGlue playbackGlue) {
         if (mPlaybackGlue != null) {
-            mPlaybackGlue.setPlayerCallback(null);
+            mPlaybackGlue.removePlayerCallback(mControlStateCallback);
         }
         mPlaybackGlue = playbackGlue;
         applyState();
@@ -234,8 +234,10 @@
     private class PlaybackControlStateCallback extends PlaybackGlue.PlayerCallback {
 
         @Override
-        public void onReadyForPlayback() {
-            internalStartPlayback();
+        public void onPreparedStateChanged(PlaybackGlue glue) {
+            if (glue.isPrepared()) {
+                internalStartPlayback();
+            }
         }
     }
 
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 4af1f98..2c4e24a 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
@@ -102,7 +102,7 @@
     void switchToVideoBeforeVideoFragmentCreated() {
         // if the video fragment is not ready: immediately fade out covering drawable,
         // hide title and mark mPendingFocusOnVideo and set focus on it later.
-        mDetailsBackgroundController.crossFadeBackgroundToVideo(true, true);
+        mDetailsBackgroundController.switchToVideoBeforeCreate();
         showTitle(false);
         mPendingFocusOnVideo = true;
         slideOutGridView();
@@ -606,6 +606,9 @@
      * @see DetailsFragmentBackgroundController#onCreateVideoFragment()
      */
     final Fragment findOrCreateVideoFragment() {
+        if (mVideoFragment != null) {
+            return mVideoFragment;
+        }
         Fragment fragment = getChildFragmentManager()
                 .findFragmentById(R.id.video_surface_container);
         if (fragment == null && mDetailsBackgroundController != null) {
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 e3fc820..71bc9aa 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
@@ -115,6 +115,7 @@
     Bitmap mCoverBitmap;
     int mSolidColor;
     boolean mCanUseHost = false;
+    boolean mInitialControlVisible = false;
 
     private Fragment mLastVideoFragmentForGlueHost;
 
@@ -241,7 +242,7 @@
         if (mCanUseHost && mPlaybackGlue != null) {
             if (playbackGlueHost == null
                     || mLastVideoFragmentForGlueHost != findOrCreateVideoFragment()) {
-                mPlaybackGlue.setHost(onCreateGlueHost());
+                mPlaybackGlue.setHost(createGlueHost());
                 mLastVideoFragmentForGlueHost = findOrCreateVideoFragment();
             } else {
                 mPlaybackGlue.setHost(playbackGlueHost);
@@ -261,7 +262,7 @@
     /**
      * Precondition allows user navigate to video fragment using DPAD. Default implementation
      * returns true if PlaybackGlue is not null. Subclass may override, e.g. only allow navigation
-     * when {@link PlaybackGlue#isReadyForPlayback()} is true. Note this method does not block
+     * when {@link PlaybackGlue#isPrepared()} is true. Note this method does not block
      * app calls {@link #switchToVideo}.
      *
      * @return True allow to navigate to video fragment.
@@ -270,8 +271,9 @@
         return mPlaybackGlue != null;
     }
 
-    void crossFadeBackgroundToVideo(boolean fadeToBackground, boolean immediate) {
-        mVideoHelper.crossFadeBackgroundToVideo(fadeToBackground, immediate);
+    void switchToVideoBeforeCreate() {
+        mVideoHelper.crossFadeBackgroundToVideo(true, true);
+        mInitialControlVisible = true;
     }
 
     /**
@@ -310,11 +312,11 @@
         if (!mCanUseHost) {
             mCanUseHost = true;
             if (mPlaybackGlue != null) {
-                mPlaybackGlue.setHost(onCreateGlueHost());
+                mPlaybackGlue.setHost(createGlueHost());
                 mLastVideoFragmentForGlueHost = findOrCreateVideoFragment();
             }
         }
-        if (mPlaybackGlue != null && mPlaybackGlue.isReadyForPlayback()) {
+        if (mPlaybackGlue != null && mPlaybackGlue.isPrepared()) {
             mPlaybackGlue.play();
         }
     }
@@ -391,6 +393,16 @@
         return new VideoFragmentGlueHost((VideoFragment) findOrCreateVideoFragment());
     }
 
+    PlaybackGlueHost createGlueHost() {
+        PlaybackGlueHost host = onCreateGlueHost();
+        if (mInitialControlVisible) {
+            host.showControlsOverlay(false);
+        } else {
+            host.hideControlsOverlay(false);
+        }
+        return host;
+    }
+
     /**
      * Adds or gets fragment for rendering video in DetailsFragment. A subclass that
      * overrides {@link #onCreateGlueHost()} should call this method to get a fragment for creating
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 31a678f..1f0c259 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
@@ -105,7 +105,7 @@
     void switchToVideoBeforeVideoSupportFragmentCreated() {
         // if the video fragment is not ready: immediately fade out covering drawable,
         // hide title and mark mPendingFocusOnVideo and set focus on it later.
-        mDetailsBackgroundController.crossFadeBackgroundToVideo(true, true);
+        mDetailsBackgroundController.switchToVideoBeforeCreate();
         showTitle(false);
         mPendingFocusOnVideo = true;
         slideOutGridView();
@@ -609,6 +609,9 @@
      * @see DetailsSupportFragmentBackgroundController#onCreateVideoSupportFragment()
      */
     final Fragment findOrCreateVideoSupportFragment() {
+        if (mVideoSupportFragment != null) {
+            return mVideoSupportFragment;
+        }
         Fragment fragment = getChildFragmentManager()
                 .findFragmentById(R.id.video_surface_container);
         if (fragment == null && mDetailsBackgroundController != null) {
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 6a951c9..accaaea 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java
@@ -118,6 +118,7 @@
     Bitmap mCoverBitmap;
     int mSolidColor;
     boolean mCanUseHost = false;
+    boolean mInitialControlVisible = false;
 
     private Fragment mLastVideoSupportFragmentForGlueHost;
 
@@ -244,7 +245,7 @@
         if (mCanUseHost && mPlaybackGlue != null) {
             if (playbackGlueHost == null
                     || mLastVideoSupportFragmentForGlueHost != findOrCreateVideoSupportFragment()) {
-                mPlaybackGlue.setHost(onCreateGlueHost());
+                mPlaybackGlue.setHost(createGlueHost());
                 mLastVideoSupportFragmentForGlueHost = findOrCreateVideoSupportFragment();
             } else {
                 mPlaybackGlue.setHost(playbackGlueHost);
@@ -264,7 +265,7 @@
     /**
      * Precondition allows user navigate to video fragment using DPAD. Default implementation
      * returns true if PlaybackGlue is not null. Subclass may override, e.g. only allow navigation
-     * when {@link PlaybackGlue#isReadyForPlayback()} is true. Note this method does not block
+     * when {@link PlaybackGlue#isPrepared()} is true. Note this method does not block
      * app calls {@link #switchToVideo}.
      *
      * @return True allow to navigate to video fragment.
@@ -273,8 +274,9 @@
         return mPlaybackGlue != null;
     }
 
-    void crossFadeBackgroundToVideo(boolean fadeToBackground, boolean immediate) {
-        mVideoHelper.crossFadeBackgroundToVideo(fadeToBackground, immediate);
+    void switchToVideoBeforeCreate() {
+        mVideoHelper.crossFadeBackgroundToVideo(true, true);
+        mInitialControlVisible = true;
     }
 
     /**
@@ -313,11 +315,11 @@
         if (!mCanUseHost) {
             mCanUseHost = true;
             if (mPlaybackGlue != null) {
-                mPlaybackGlue.setHost(onCreateGlueHost());
+                mPlaybackGlue.setHost(createGlueHost());
                 mLastVideoSupportFragmentForGlueHost = findOrCreateVideoSupportFragment();
             }
         }
-        if (mPlaybackGlue != null && mPlaybackGlue.isReadyForPlayback()) {
+        if (mPlaybackGlue != null && mPlaybackGlue.isPrepared()) {
             mPlaybackGlue.play();
         }
     }
@@ -394,6 +396,16 @@
         return new VideoSupportFragmentGlueHost((VideoSupportFragment) findOrCreateVideoSupportFragment());
     }
 
+    PlaybackGlueHost createGlueHost() {
+        PlaybackGlueHost host = onCreateGlueHost();
+        if (mInitialControlVisible) {
+            host.showControlsOverlay(false);
+        } else {
+            host.hideControlsOverlay(false);
+        }
+        return host;
+    }
+
     /**
      * Adds or gets fragment for rendering video in DetailsSupportFragment. A subclass that
      * overrides {@link #onCreateGlueHost()} should call this method to get a fragment for creating
diff --git a/v17/leanback/src/android/support/v17/leanback/app/OnboardingSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/OnboardingSupportFragment.java
index 8009e3f..0431c18 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/OnboardingSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/OnboardingSupportFragment.java
@@ -398,6 +398,7 @@
      * Returns the text color of TitleView if it's set through
      * {@link #setTitleViewTextColor(int)}. If no color was set, transparent is returned.
      */
+    @ColorInt
     public final int getTitleViewTextColor() {
         return mTitleViewTextColor;
     }
@@ -419,6 +420,7 @@
      * Returns the text color of DescriptionView if it's set through
      * {@link #setDescriptionViewTextColor(int)}. If no color was set, transparent is returned.
      */
+    @ColorInt
     public final int getDescriptionViewTextColor() {
         return mDescriptionViewTextColor;
     }
@@ -439,6 +441,7 @@
      * Returns the background color of the dot if it's set through
      * {@link #setDotBackgroundColor(int)}. If no color was set, transparent is returned.
      */
+    @ColorInt
     public final int getDotBackgroundColor() {
         return mDotBackgroundColor;
     }
@@ -460,6 +463,7 @@
      * Returns the background color of the arrow if it's set through
      * {@link #setArrowBackgroundColor(int)}. If no color was set, transparent is returned.
      */
+    @ColorInt
     public final int getArrowBackgroundColor() {
         return mArrowBackgroundColor;
     }
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 60cd06b..68a1215 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
@@ -37,6 +37,8 @@
 import android.support.v17.leanback.widget.ItemBridgeAdapter;
 import android.support.v17.leanback.widget.ObjectAdapter;
 import android.support.v17.leanback.widget.PlaybackRowPresenter;
+import android.support.v17.leanback.widget.PlaybackSeekDataProvider;
+import android.support.v17.leanback.widget.PlaybackSeekUi;
 import android.support.v17.leanback.widget.Presenter;
 import android.support.v17.leanback.widget.PresenterSelector;
 import android.support.v17.leanback.widget.Row;
@@ -53,8 +55,6 @@
 import android.view.ViewGroup;
 import android.view.animation.AccelerateInterpolator;
 
-import java.util.ArrayList;
-
 /**
  * A fragment for displaying playback controls and related content.
  *
@@ -71,8 +71,15 @@
  * optional, app can pass playback row and PlaybackRowPresenter in the adapter using
  * {@link #setAdapter(ObjectAdapter)}.
  * </p>
+ * <p>
+ * Auto hide controls upon playing: best practice is calling
+ * {@link #setControlsOverlayAutoHideEnabled(boolean)} upon play/pause. The auto hiding timer will
+ * be cancelled upon {@link #tickle()} triggered by input event.
+ * </p>
  */
 public class PlaybackFragment extends Fragment {
+    static final String BUNDLE_CONTROL_VISIBLE_ON_CREATEVIEW = "controlvisible_oncreateview";
+
     /**
      * No background.
      */
@@ -84,6 +91,10 @@
     public static final int BG_DARK = 1;
     PlaybackGlueHost.HostCallback mHostCallback;
 
+    PlaybackSeekUi.Client mSeekUiClient;
+    boolean mInSeek;
+    ProgressBarManager mProgressBarManager = new ProgressBarManager();
+
     /**
      * Resets the focus on the button in the middle of control row.
      * @hide
@@ -182,12 +193,12 @@
 
     // Fading status
     private static final int IDLE = 0;
-    private static final int IN = 1;
-    private static final int OUT = 2;
+    private static final int ANIMATING = 1;
 
     int mPaddingBottom;
     int mOtherRowsCenterToBottom;
     View mRootView;
+    View mBackgroundView;
     int mBackgroundType = BG_DARK;
     int mBgDarkColor;
     int mBgLightColor;
@@ -197,7 +208,8 @@
     OnFadeCompleteListener mFadeCompleteListener;
     View.OnKeyListener mInputEventHandler;
     boolean mFadingEnabled = true;
-    int mFadingStatus = IDLE;
+    boolean mControlVisibleBeforeOnCreateView = true;
+    boolean mControlVisible = true;
     int mBgAlpha;
     ValueAnimator mBgFadeInAnimator, mBgFadeOutAnimator;
     ValueAnimator mControlRowFadeInAnimator, mControlRowFadeOutAnimator;
@@ -223,7 +235,6 @@
                     if (DEBUG) Log.v(TAG, "onAnimationEnd " + mBgAlpha);
                     if (mBgAlpha > 0) {
                         enableVerticalGridAnimations(true);
-                        startFadeTimer();
                         if (mFadeCompleteListener != null) {
                             mFadeCompleteListener.onFadeInComplete();
                         }
@@ -242,10 +253,13 @@
                             mFadeCompleteListener.onFadeOutComplete();
                         }
                     }
-                    mFadingStatus = IDLE;
                 }
             };
 
+    public PlaybackFragment() {
+        mProgressBarManager.setInitialDelay(500);
+    }
+
     VerticalGridView getVerticalGridView() {
         if (mRowsFragment == null) {
             return null;
@@ -257,7 +271,7 @@
         @Override
         public void handleMessage(Message message) {
             if (message.what == START_FADE_OUT && mFadingEnabled) {
-                fade(false);
+                hideControlsOverlay(true);
             }
         }
     };
@@ -280,8 +294,8 @@
 
     private void setBgAlpha(int alpha) {
         mBgAlpha = alpha;
-        if (mRootView != null) {
-            mRootView.getBackground().setAlpha(alpha);
+        if (mBackgroundView != null) {
+            mBackgroundView.getBackground().setAlpha(alpha);
         }
     }
 
@@ -292,36 +306,54 @@
     }
 
     /**
-     * Enables or disables view fading.  If enabled,
-     * the view will be faded in when the fragment starts,
-     * and will fade out after a time period.  The timeout
-     * period is reset each time {@link #tickle} is called.
+     * Enables or disables auto hiding controls overlay after a short delay fragment is resumed.
+     * If enabled and fragment is resumed, the view will fade out after a time period.
+     * {@link #tickle()} will kill the timer, next time fragment is resumed,
+     * the timer will be started again if {@link #isControlsOverlayAutoHideEnabled()} is true.
      */
-    public void setFadingEnabled(boolean enabled) {
-        if (DEBUG) Log.v(TAG, "setFadingEnabled " + enabled);
+    public void setControlsOverlayAutoHideEnabled(boolean enabled) {
+        if (DEBUG) Log.v(TAG, "setControlsOverlayAutoHideEnabled " + enabled);
         if (enabled != mFadingEnabled) {
             mFadingEnabled = enabled;
-            if (mFadingEnabled) {
-                if (isResumed() && mFadingStatus == IDLE
-                        && !mHandler.hasMessages(START_FADE_OUT)) {
+            if (isResumed() && getView().hasFocus()) {
+                showControlsOverlay(true);
+                if (enabled) {
+                    // StateGraph 7->2 5->2
                     startFadeTimer();
+                } else {
+                    // StateGraph 4->5 2->5
+                    stopFadeTimer();
                 }
             } else {
-                // Ensure fully opaque
-                mHandler.removeMessages(START_FADE_OUT);
-                fade(true);
+                // StateGraph 6->1 1->6
             }
         }
     }
 
     /**
-     * Returns true if view fading is enabled.
+     * Returns true if controls will be auto hidden after a delay when fragment is resumed.
      */
-    public boolean isFadingEnabled() {
+    public boolean isControlsOverlayAutoHideEnabled() {
         return mFadingEnabled;
     }
 
     /**
+     * @deprecated Uses {@link #setControlsOverlayAutoHideEnabled(boolean)}
+     */
+    @Deprecated
+    public void setFadingEnabled(boolean enabled) {
+        setControlsOverlayAutoHideEnabled(enabled);
+    }
+
+    /**
+     * @deprecated Uses {@link #isControlsOverlayAutoHideEnabled()}
+     */
+    @Deprecated
+    public boolean isFadingEnabled() {
+        return isControlsOverlayAutoHideEnabled();
+    }
+
+    /**
      * Sets the listener to be called when fade in or out has completed.
      * @hide
      */
@@ -345,46 +377,29 @@
     }
 
     /**
-     * Tickles the playback controls.  Fades in the view if it was faded out,
-     * otherwise resets the fade out timer.  Tickling on input events is handled
-     * by the fragment.
+     * Tickles the playback controls. Fades in the view if it was faded out. {@link #tickle()} will
+     * also kill the timer created by {@link #setControlsOverlayAutoHideEnabled(boolean)}. When
+     * next time fragment is resumed, the timer will be started again if
+     * {@link #isControlsOverlayAutoHideEnabled()} is true. In most cases app does not need call
+     * this method, tickling on input events is handled by the fragment.
      */
     public void tickle() {
         if (DEBUG) Log.v(TAG, "tickle enabled " + mFadingEnabled + " isResumed " + isResumed());
-        if (!mFadingEnabled || !isResumed()) {
-            return;
-        }
-        if (mHandler.hasMessages(START_FADE_OUT)) {
-            // Restart the timer
-            startFadeTimer();
-        } else {
-            fade(true);
-        }
-    }
-
-    /**
-     * Fades out the playback overlay immediately.
-     */
-    public void fadeOut() {
-        mHandler.removeMessages(START_FADE_OUT);
-        fade(false);
-    }
-
-    /**
-     * Returns true/false indicating whether playback controls are visible or not.
-     */
-    private boolean areControlsHidden() {
-        return mFadingStatus == IDLE && mBgAlpha == 0;
+        //StateGraph 2->4
+        stopFadeTimer();
+        showControlsOverlay(true);
     }
 
     private boolean onInterceptInputEvent(InputEvent event) {
-        final boolean controlsHidden = areControlsHidden();
+        final boolean controlsHidden = !mControlVisible;
         if (DEBUG) Log.v(TAG, "onInterceptInputEvent hidden " + controlsHidden + " " + event);
         boolean consumeEvent = false;
         int keyCode = KeyEvent.KEYCODE_UNKNOWN;
+        int keyAction = 0;
 
         if (event instanceof KeyEvent) {
             keyCode = ((KeyEvent) event).getKeyCode();
+            keyAction = ((KeyEvent) event).getAction();
             if (mInputEventHandler != null) {
                 consumeEvent = mInputEventHandler.onKey(getView(), keyCode, (KeyEvent) event);
             }
@@ -401,34 +416,60 @@
                 if (controlsHidden) {
                     consumeEvent = true;
                 }
-                tickle();
+                if (keyAction == KeyEvent.ACTION_DOWN) {
+                    tickle();
+                }
                 break;
             case KeyEvent.KEYCODE_BACK:
             case KeyEvent.KEYCODE_ESCAPE:
-                // If fading enabled and controls are not hidden, back will be consumed to fade
+                if (mInSeek) {
+                    // when in seek, the SeekUi will handle the BACK.
+                    return false;
+                }
+                // If controls are not hidden, back will be consumed to fade
                 // them out (even if the key was consumed by the handler).
-                if (mFadingEnabled && !controlsHidden) {
+                if (!controlsHidden) {
                     consumeEvent = true;
-                    mHandler.removeMessages(START_FADE_OUT);
-                    fade(false);
-                } else if (consumeEvent) {
-                    tickle();
+
+                    if (((KeyEvent) event).getAction() == KeyEvent.ACTION_UP) {
+                        hideControlsOverlay(true);
+                    }
                 }
                 break;
             default:
                 if (consumeEvent) {
-                    tickle();
+                    if (keyAction == KeyEvent.ACTION_DOWN) {
+                        tickle();
+                    }
                 }
         }
         return consumeEvent;
     }
 
     @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        // controls view are initially visible, make it invisible
+        // if app has called hideControlsOverlay() before view created.
+        mControlVisible = true;
+        if (!mControlVisibleBeforeOnCreateView) {
+            showControlsOverlay(false, false);
+            mControlVisibleBeforeOnCreateView = true;
+        }
+    }
+
+    @Override
     public void onResume() {
         super.onResume();
-        if (mFadingEnabled) {
-            setBgAlpha(0);
-            fade(true);
+
+        if (mControlVisible) {
+            //StateGraph: 6->5 1->2
+            if (mFadingEnabled) {
+                // StateGraph 1->2
+                startFadeTimer();
+            }
+        } else {
+            //StateGraph: 6->7 1->3
         }
         getVerticalGridView().setOnTouchInterceptListener(mOnTouchInterceptListener);
         getVerticalGridView().setOnKeyInterceptListener(mOnKeyInterceptListener);
@@ -437,6 +478,12 @@
         }
     }
 
+    private void stopFadeTimer() {
+        if (mHandler != null) {
+            mHandler.removeMessages(START_FADE_OUT);
+        }
+    }
+
     private void startFadeTimer() {
         if (mHandler != null) {
             mHandler.removeMessages(START_FADE_OUT);
@@ -471,31 +518,19 @@
     private TimeInterpolator mLogDecelerateInterpolator = new LogDecelerateInterpolator(100, 0);
     private TimeInterpolator mLogAccelerateInterpolator = new LogAccelerateInterpolator(100, 0);
 
-    private View getControlRowView() {
-        if (getVerticalGridView() == null) {
-            return null;
-        }
-        RecyclerView.ViewHolder vh = getVerticalGridView().findViewHolderForPosition(0);
-        if (vh == null) {
-            return null;
-        }
-        return vh.itemView;
-    }
-
     private void loadControlRowAnimator() {
-        final AnimatorListener listener = new AnimatorListener() {
-            @Override
-            void getViews(ArrayList<View> views) {
-                View view = getControlRowView();
-                if (view != null) {
-                    views.add(view);
-                }
-            }
-        };
         final AnimatorUpdateListener updateListener = new AnimatorUpdateListener() {
             @Override
             public void onAnimationUpdate(ValueAnimator arg0) {
-                View view = getControlRowView();
+                if (getVerticalGridView() == null) {
+                    return;
+                }
+                RecyclerView.ViewHolder vh = getVerticalGridView()
+                        .findViewHolderForAdapterPosition(0);
+                if (vh == null) {
+                    return;
+                }
+                View view = vh.itemView;
                 if (view != null) {
                     final float fraction = (Float) arg0.getAnimatedValue();
                     if (DEBUG) Log.v(TAG, "fraction " + fraction);
@@ -508,32 +543,15 @@
         Context context = FragmentUtil.getContext(this);
         mControlRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in);
         mControlRowFadeInAnimator.addUpdateListener(updateListener);
-        mControlRowFadeInAnimator.addListener(listener);
         mControlRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator);
 
         mControlRowFadeOutAnimator = loadAnimator(context,
                 R.animator.lb_playback_controls_fade_out);
         mControlRowFadeOutAnimator.addUpdateListener(updateListener);
-        mControlRowFadeOutAnimator.addListener(listener);
         mControlRowFadeOutAnimator.setInterpolator(mLogAccelerateInterpolator);
     }
 
     private void loadOtherRowAnimator() {
-        final AnimatorListener listener = new AnimatorListener() {
-            @Override
-            void getViews(ArrayList<View> views) {
-                if (getVerticalGridView() == null) {
-                    return;
-                }
-                final int count = getVerticalGridView().getChildCount();
-                for (int i = 0; i < count; i++) {
-                    View view = getVerticalGridView().getChildAt(i);
-                    if (view != null) {
-                        views.add(view);
-                    }
-                }
-            }
-        };
         final AnimatorUpdateListener updateListener = new AnimatorUpdateListener() {
             @Override
             public void onAnimationUpdate(ValueAnimator arg0) {
@@ -541,8 +559,10 @@
                     return;
                 }
                 final float fraction = (Float) arg0.getAnimatedValue();
-                for (View view : listener.mViews) {
-                    if (getVerticalGridView().getChildPosition(view) > 0) {
+                final int count = getVerticalGridView().getChildCount();
+                for (int i = 0; i < count; i++) {
+                    View view = getVerticalGridView().getChildAt(i);
+                    if (getVerticalGridView().getChildAdapterPosition(view) > 0) {
                         view.setAlpha(fraction);
                         view.setTranslationY((float) mAnimationTranslateY * (1f - fraction));
                     }
@@ -552,67 +572,133 @@
 
         Context context = FragmentUtil.getContext(this);
         mOtherRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in);
-        mOtherRowFadeInAnimator.addListener(listener);
         mOtherRowFadeInAnimator.addUpdateListener(updateListener);
         mOtherRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator);
 
         mOtherRowFadeOutAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_out);
-        mOtherRowFadeOutAnimator.addListener(listener);
         mOtherRowFadeOutAnimator.addUpdateListener(updateListener);
         mOtherRowFadeOutAnimator.setInterpolator(new AccelerateInterpolator());
     }
 
-    private void fade(boolean fadeIn) {
-        if (DEBUG) Log.v(TAG, "fade " + fadeIn);
-        if (getView() == null) {
-            return;
-        }
-        if ((fadeIn && mFadingStatus == IN) || (!fadeIn && mFadingStatus == OUT)) {
-            if (DEBUG) Log.v(TAG, "requested fade in progress");
-            return;
-        }
-        if ((fadeIn && mBgAlpha == 255) || (!fadeIn && mBgAlpha == 0)) {
-            if (DEBUG) Log.v(TAG, "fade is no-op");
-            return;
-        }
+    /**
+     * Fades out the playback overlay immediately.
+     * @deprecated Call {@link #hideControlsOverlay(boolean)}
+     */
+    @Deprecated
+    public void fadeOut() {
+        showControlsOverlay(false, false);
+    }
 
-        mAnimationTranslateY = getVerticalGridView().getSelectedPosition() == 0
-                ? mMajorFadeTranslateY : mMinorFadeTranslateY;
+    /**
+     * Show controls overlay.
+     *
+     * @param runAnimation True to run animation, false otherwise.
+     */
+    public void showControlsOverlay(boolean runAnimation) {
+        showControlsOverlay(true, runAnimation);
+    }
 
-        if (mFadingStatus == IDLE) {
-            if (fadeIn) {
-                mBgFadeInAnimator.start();
-                mControlRowFadeInAnimator.start();
-                mOtherRowFadeInAnimator.start();
-            } else {
-                mBgFadeOutAnimator.start();
-                mControlRowFadeOutAnimator.start();
-                mOtherRowFadeOutAnimator.start();
+    /**
+     * Returns true if controls overlay is visible, false otherwise.
+     *
+     * @return True if controls overlay is visible, false otherwise.
+     * @see #showControlsOverlay(boolean)
+     * @see #hideControlsOverlay(boolean)
+     */
+    public boolean isControlsOverlayVisible() {
+        return mControlVisible;
+    }
+
+    /**
+     * Hide controls overlay.
+     *
+     * @param runAnimation True to run animation, false otherwise.
+     */
+    public void hideControlsOverlay(boolean runAnimation) {
+        showControlsOverlay(false, runAnimation);
+    }
+
+    /**
+     * if first animator is still running, reverse it; otherwise start second animator.
+     */
+    static void reverseFirstOrStartSecond(ValueAnimator first, ValueAnimator second,
+            boolean runAnimation) {
+        if (first.isStarted()) {
+            first.reverse();
+            if (!runAnimation) {
+                first.end();
             }
         } else {
-            if (fadeIn) {
-                mBgFadeOutAnimator.reverse();
-                mControlRowFadeOutAnimator.reverse();
-                mOtherRowFadeOutAnimator.reverse();
-            } else {
-                mBgFadeInAnimator.reverse();
-                mControlRowFadeInAnimator.reverse();
-                mOtherRowFadeInAnimator.reverse();
+            second.start();
+            if (!runAnimation) {
+                second.end();
             }
         }
-        getView().announceForAccessibility(getString(fadeIn ? R.string.lb_playback_controls_shown
-                : R.string.lb_playback_controls_hidden));
+    }
 
-        // If fading in while control row is focused, set initial translationY so
-        // views slide in from below.
-        if (fadeIn && mFadingStatus == IDLE) {
-            final int count = getVerticalGridView().getChildCount();
-            for (int i = 0; i < count; i++) {
-                getVerticalGridView().getChildAt(i).setTranslationY(mAnimationTranslateY);
+    /**
+     * End first or second animator if they are still running.
+     */
+    static void endAll(ValueAnimator first, ValueAnimator second) {
+        if (first.isStarted()) {
+            first.end();
+        } else if (second.isStarted()) {
+            second.end();
+        }
+    }
+
+    /**
+     * Fade in or fade out rows and background.
+     *
+     * @param show True to fade in, false to fade out.
+     * @param animation True to run animation.
+     */
+    void showControlsOverlay(boolean show, boolean animation) {
+        if (DEBUG) Log.v(TAG, "showControlsOverlay " + show);
+        if (getView() == null) {
+            mControlVisibleBeforeOnCreateView = show;
+            return;
+        }
+        // force no animation when fragment is not resumed
+        if (!isResumed()) {
+            animation = false;
+        }
+        if (show == mControlVisible) {
+            if (!animation) {
+                // End animation if needed
+                endAll(mBgFadeInAnimator, mBgFadeOutAnimator);
+                endAll(mControlRowFadeInAnimator, mControlRowFadeOutAnimator);
+                endAll(mOtherRowFadeInAnimator, mOtherRowFadeOutAnimator);
             }
+            return;
+        }
+        // StateGraph: 7<->5 4<->3 2->3
+        mControlVisible = show;
+        if (!mControlVisible) {
+            // StateGraph 2->3
+            stopFadeTimer();
         }
 
-        mFadingStatus = fadeIn ? IN : OUT;
+        mAnimationTranslateY = (getVerticalGridView() == null
+                || getVerticalGridView().getSelectedPosition() == 0)
+                ? mMajorFadeTranslateY : mMinorFadeTranslateY;
+
+        if (show) {
+            reverseFirstOrStartSecond(mBgFadeOutAnimator, mBgFadeInAnimator, animation);
+            reverseFirstOrStartSecond(mControlRowFadeOutAnimator, mControlRowFadeInAnimator,
+                    animation);
+            reverseFirstOrStartSecond(mOtherRowFadeOutAnimator, mOtherRowFadeInAnimator, animation);
+        } else {
+            reverseFirstOrStartSecond(mBgFadeInAnimator, mBgFadeOutAnimator, animation);
+            reverseFirstOrStartSecond(mControlRowFadeInAnimator, mControlRowFadeOutAnimator,
+                    animation);
+            reverseFirstOrStartSecond(mOtherRowFadeInAnimator, mOtherRowFadeOutAnimator, animation);
+        }
+        if (animation) {
+            getView().announceForAccessibility(getString(show
+                    ? R.string.lb_playback_controls_shown
+                    : R.string.lb_playback_controls_hidden));
+        }
     }
 
     /**
@@ -711,7 +797,7 @@
     }
 
     private void updateBackground() {
-        if (mRootView != null) {
+        if (mBackgroundView != null) {
             int color = mBgDarkColor;
             switch (mBackgroundType) {
                 case BG_DARK:
@@ -723,7 +809,8 @@
                     color = Color.TRANSPARENT;
                     break;
             }
-            mRootView.setBackground(new ColorDrawable(color));
+            mBackgroundView.setBackground(new ColorDrawable(color));
+            setBgAlpha(mBgAlpha);
         }
     }
 
@@ -732,13 +819,21 @@
                 @Override
                 public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder vh) {
                     if (DEBUG) Log.v(TAG, "onAttachedToWindow " + vh.getViewHolder().view);
-                    if ((mFadingStatus == IDLE && mBgAlpha == 0) || mFadingStatus == OUT) {
+                    if (!mControlVisible) {
                         if (DEBUG) Log.v(TAG, "setting alpha to 0");
                         vh.getViewHolder().view.setAlpha(0);
                     }
                 }
 
                 @Override
+                public void onCreate(ItemBridgeAdapter.ViewHolder vh) {
+                    Presenter.ViewHolder viewHolder = vh.getViewHolder();
+                    if (viewHolder instanceof PlaybackSeekUi) {
+                        ((PlaybackSeekUi) viewHolder).setPlaybackSeekUiClient(mChainedClient);
+                    }
+                }
+
+                @Override
                 public void onDetachedFromWindow(ItemBridgeAdapter.ViewHolder vh) {
                     if (DEBUG) Log.v(TAG, "onDetachedFromWindow " + vh.getViewHolder().view);
                     // Reset animation state
@@ -756,6 +851,7 @@
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
                              Bundle savedInstanceState) {
         mRootView = inflater.inflate(R.layout.lb_playback_fragment, container, false);
+        mBackgroundView = mRootView.findViewById(R.id.playback_fragment_background);
         mRowsFragment = (RowsFragment) getChildFragmentManager().findFragmentById(
                 R.id.playback_controls_dock);
         if (mRowsFragment == null) {
@@ -775,6 +871,10 @@
         mBgAlpha = 255;
         updateBackground();
         mRowsFragment.setExternalAdapterListener(mAdapterListener);
+        ProgressBarManager progressBarManager = getProgressBarManager();
+        if (progressBarManager != null) {
+            progressBarManager.setRootView((ViewGroup) mRootView);
+        }
         return mRootView;
     }
 
@@ -809,6 +909,12 @@
         if (mHostCallback != null) {
             mHostCallback.onHostPause();
         }
+        if (mHandler.hasMessages(START_FADE_OUT)) {
+            // StateGraph: 2->1
+            mHandler.removeMessages(START_FADE_OUT);
+        } else {
+            // StateGraph: 5->6, 7->6, 4->1, 3->1
+        }
         super.onPause();
     }
 
@@ -839,6 +945,7 @@
     @Override
     public void onDestroyView() {
         mRootView = null;
+        mBackgroundView = null;
         super.onDestroyView();
     }
 
@@ -952,36 +1059,111 @@
         }
     }
 
-    static abstract class AnimatorListener implements Animator.AnimatorListener {
-        ArrayList<View> mViews = new ArrayList<View>();
-        ArrayList<Integer> mLayerType = new ArrayList<Integer>();
-
+    final PlaybackSeekUi.Client mChainedClient = new PlaybackSeekUi.Client() {
         @Override
-        public void onAnimationCancel(Animator animation) {
+        public boolean isSeekEnabled() {
+            return mSeekUiClient == null ? false : mSeekUiClient.isSeekEnabled();
         }
 
         @Override
-        public void onAnimationRepeat(Animator animation) {
+        public void onSeekStarted() {
+            if (mSeekUiClient != null) {
+                mSeekUiClient.onSeekStarted();
+            }
+            setSeekMode(true);
         }
 
         @Override
-        public void onAnimationStart(Animator animation) {
-            getViews(mViews);
-            for (View view : mViews) {
-                mLayerType.add(view.getLayerType());
-                view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+        public PlaybackSeekDataProvider getPlaybackSeekDataProvider() {
+            return mSeekUiClient == null ? null : mSeekUiClient.getPlaybackSeekDataProvider();
+        }
+
+        @Override
+        public void onSeekPositionChanged(long pos) {
+            if (mSeekUiClient != null) {
+                mSeekUiClient.onSeekPositionChanged(pos);
             }
         }
 
         @Override
-        public void onAnimationEnd(Animator animation) {
-            for (int i = 0; i < mViews.size(); i++) {
-                mViews.get(i).setLayerType(mLayerType.get(i), null);
+        public void onSeekFinished(boolean cancelled) {
+            if (mSeekUiClient != null) {
+                mSeekUiClient.onSeekFinished(cancelled);
             }
-            mLayerType.clear();
-            mViews.clear();
+            setSeekMode(false);
         }
+    };
 
-        abstract void getViews(ArrayList<View> views);
+    /**
+     * Interface to be implemented by UI widget to support PlaybackSeekUi.
+     */
+    public void setPlaybackSeekUiClient(PlaybackSeekUi.Client client) {
+        mSeekUiClient = client;
+    }
+
+    /**
+     * Show or hide other rows other than PlaybackRow.
+     * @param inSeek True to make other rows visible, false to make other rows invisible.
+     */
+    void setSeekMode(boolean inSeek) {
+        if (mInSeek == inSeek) {
+            return;
+        }
+        mInSeek = inSeek;
+        getVerticalGridView().setSelectedPosition(0);
+        if (mInSeek) {
+            stopFadeTimer();
+        }
+        // immediately fade in control row.
+        showControlsOverlay(true);
+        final int count = getVerticalGridView().getChildCount();
+        for (int i = 0; i < count; i++) {
+            View view = getVerticalGridView().getChildAt(i);
+            if (getVerticalGridView().getChildAdapterPosition(view) > 0) {
+                view.setVisibility(mInSeek ? View.INVISIBLE : View.VISIBLE);
+            }
+        }
+    }
+
+    /**
+     * Called when size of the video changes. App may override.
+     * @param videoWidth Intrinsic width of video
+     * @param videoHeight Intrinsic height of video
+     */
+    protected void onVideoSizeChanged(int videoWidth, int videoHeight) {
+    }
+
+    /**
+     * Called when media has start or stop buffering. App may override. The default initial state
+     * is not buffering.
+     * @param start True for buffering start, false otherwise.
+     */
+    protected void onBufferingStateChanged(boolean start) {
+        ProgressBarManager progressBarManager = getProgressBarManager();
+        if (progressBarManager != null) {
+            if (start) {
+                progressBarManager.show();
+            } else {
+                progressBarManager.hide();
+            }
+        }
+    }
+
+    /**
+     * Called when media has error. App may override.
+     * @param errorCode Optional error code for specific implementation.
+     * @param errorMessage Optional error message for specific implementation.
+     */
+    protected void onError(int errorCode, CharSequence errorMessage) {
+    }
+
+    /**
+     * Returns the ProgressBarManager that will show or hide progress bar in
+     * {@link #onBufferingStateChanged(boolean)}.
+     * @return The ProgressBarManager that will show or hide progress bar in
+     * {@link #onBufferingStateChanged(boolean)}.
+     */
+    public ProgressBarManager getProgressBarManager() {
+        return mProgressBarManager;
     }
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragmentGlueHost.java b/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragmentGlueHost.java
index fdaa6ef..d537c3a 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragmentGlueHost.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragmentGlueHost.java
@@ -18,6 +18,7 @@
 import android.support.v17.leanback.widget.OnActionClickedListener;
 import android.support.v17.leanback.widget.OnItemViewClickedListener;
 import android.support.v17.leanback.widget.PlaybackRowPresenter;
+import android.support.v17.leanback.widget.PlaybackSeekUi;
 import android.support.v17.leanback.widget.Presenter;
 import android.support.v17.leanback.widget.Row;
 import android.support.v17.leanback.widget.RowPresenter;
@@ -27,7 +28,7 @@
  * {@link PlaybackGlueHost} implementation
  * the interaction between this class and {@link PlaybackFragment}.
  */
-public class PlaybackFragmentGlueHost extends PlaybackGlueHost {
+public class PlaybackFragmentGlueHost extends PlaybackGlueHost implements PlaybackSeekUi {
     private final PlaybackFragment mFragment;
 
     public PlaybackFragmentGlueHost(PlaybackFragment fragment) {
@@ -35,8 +36,13 @@
     }
 
     @Override
-    public void setFadingEnabled(boolean enable) {
-        mFragment.setFadingEnabled(enable);
+    public void setControlsOverlayAutoHideEnabled(boolean enabled) {
+        mFragment.setControlsOverlayAutoHideEnabled(enabled);
+    }
+
+    @Override
+    public boolean isControlsOverlayAutoHideEnabled() {
+        return mFragment.isControlsOverlayAutoHideEnabled();
     }
 
     @Override
@@ -85,4 +91,47 @@
     public void fadeOut() {
         mFragment.fadeOut();
     }
+
+    @Override
+    public boolean isControlsOverlayVisible() {
+        return mFragment.isControlsOverlayVisible();
+    }
+
+    @Override
+    public void hideControlsOverlay(boolean runAnimation) {
+        mFragment.hideControlsOverlay(runAnimation);
+    }
+
+    @Override
+    public void showControlsOverlay(boolean runAnimation) {
+        mFragment.showControlsOverlay(runAnimation);
+    }
+
+    @Override
+    public void setPlaybackSeekUiClient(Client client) {
+        mFragment.setPlaybackSeekUiClient(client);
+    }
+
+    final PlayerCallback mPlayerCallback =
+            new PlayerCallback() {
+                @Override
+                public void onBufferingStateChanged(boolean start) {
+                    mFragment.onBufferingStateChanged(start);
+                }
+
+                @Override
+                public void onError(int errorCode, CharSequence errorMessage) {
+                    mFragment.onError(errorCode, errorMessage);
+                }
+
+                @Override
+                public void onVideoSizeChanged(int videoWidth, int videoHeight) {
+                    mFragment.onVideoSizeChanged(videoWidth, videoHeight);
+                }
+            };
+
+    @Override
+    public PlayerCallback getPlayerCallback() {
+        return mPlayerCallback;
+    }
 }
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 81e76a6..d63e72c 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java
@@ -40,6 +40,8 @@
 import android.support.v17.leanback.widget.ItemBridgeAdapter;
 import android.support.v17.leanback.widget.ObjectAdapter;
 import android.support.v17.leanback.widget.PlaybackRowPresenter;
+import android.support.v17.leanback.widget.PlaybackSeekDataProvider;
+import android.support.v17.leanback.widget.PlaybackSeekUi;
 import android.support.v17.leanback.widget.Presenter;
 import android.support.v17.leanback.widget.PresenterSelector;
 import android.support.v17.leanback.widget.Row;
@@ -56,8 +58,6 @@
 import android.view.ViewGroup;
 import android.view.animation.AccelerateInterpolator;
 
-import java.util.ArrayList;
-
 /**
  * A fragment for displaying playback controls and related content.
  *
@@ -74,8 +74,15 @@
  * optional, app can pass playback row and PlaybackRowPresenter in the adapter using
  * {@link #setAdapter(ObjectAdapter)}.
  * </p>
+ * <p>
+ * Auto hide controls upon playing: best practice is calling
+ * {@link #setControlsOverlayAutoHideEnabled(boolean)} upon play/pause. The auto hiding timer will
+ * be cancelled upon {@link #tickle()} triggered by input event.
+ * </p>
  */
 public class PlaybackSupportFragment extends Fragment {
+    static final String BUNDLE_CONTROL_VISIBLE_ON_CREATEVIEW = "controlvisible_oncreateview";
+
     /**
      * No background.
      */
@@ -87,6 +94,10 @@
     public static final int BG_DARK = 1;
     PlaybackGlueHost.HostCallback mHostCallback;
 
+    PlaybackSeekUi.Client mSeekUiClient;
+    boolean mInSeek;
+    ProgressBarManager mProgressBarManager = new ProgressBarManager();
+
     /**
      * Resets the focus on the button in the middle of control row.
      * @hide
@@ -185,12 +196,12 @@
 
     // Fading status
     private static final int IDLE = 0;
-    private static final int IN = 1;
-    private static final int OUT = 2;
+    private static final int ANIMATING = 1;
 
     int mPaddingBottom;
     int mOtherRowsCenterToBottom;
     View mRootView;
+    View mBackgroundView;
     int mBackgroundType = BG_DARK;
     int mBgDarkColor;
     int mBgLightColor;
@@ -200,7 +211,8 @@
     OnFadeCompleteListener mFadeCompleteListener;
     View.OnKeyListener mInputEventHandler;
     boolean mFadingEnabled = true;
-    int mFadingStatus = IDLE;
+    boolean mControlVisibleBeforeOnCreateView = true;
+    boolean mControlVisible = true;
     int mBgAlpha;
     ValueAnimator mBgFadeInAnimator, mBgFadeOutAnimator;
     ValueAnimator mControlRowFadeInAnimator, mControlRowFadeOutAnimator;
@@ -226,7 +238,6 @@
                     if (DEBUG) Log.v(TAG, "onAnimationEnd " + mBgAlpha);
                     if (mBgAlpha > 0) {
                         enableVerticalGridAnimations(true);
-                        startFadeTimer();
                         if (mFadeCompleteListener != null) {
                             mFadeCompleteListener.onFadeInComplete();
                         }
@@ -245,10 +256,13 @@
                             mFadeCompleteListener.onFadeOutComplete();
                         }
                     }
-                    mFadingStatus = IDLE;
                 }
             };
 
+    public PlaybackSupportFragment() {
+        mProgressBarManager.setInitialDelay(500);
+    }
+
     VerticalGridView getVerticalGridView() {
         if (mRowsSupportFragment == null) {
             return null;
@@ -260,7 +274,7 @@
         @Override
         public void handleMessage(Message message) {
             if (message.what == START_FADE_OUT && mFadingEnabled) {
-                fade(false);
+                hideControlsOverlay(true);
             }
         }
     };
@@ -283,8 +297,8 @@
 
     private void setBgAlpha(int alpha) {
         mBgAlpha = alpha;
-        if (mRootView != null) {
-            mRootView.getBackground().setAlpha(alpha);
+        if (mBackgroundView != null) {
+            mBackgroundView.getBackground().setAlpha(alpha);
         }
     }
 
@@ -295,36 +309,54 @@
     }
 
     /**
-     * Enables or disables view fading.  If enabled,
-     * the view will be faded in when the fragment starts,
-     * and will fade out after a time period.  The timeout
-     * period is reset each time {@link #tickle} is called.
+     * Enables or disables auto hiding controls overlay after a short delay fragment is resumed.
+     * If enabled and fragment is resumed, the view will fade out after a time period.
+     * {@link #tickle()} will kill the timer, next time fragment is resumed,
+     * the timer will be started again if {@link #isControlsOverlayAutoHideEnabled()} is true.
      */
-    public void setFadingEnabled(boolean enabled) {
-        if (DEBUG) Log.v(TAG, "setFadingEnabled " + enabled);
+    public void setControlsOverlayAutoHideEnabled(boolean enabled) {
+        if (DEBUG) Log.v(TAG, "setControlsOverlayAutoHideEnabled " + enabled);
         if (enabled != mFadingEnabled) {
             mFadingEnabled = enabled;
-            if (mFadingEnabled) {
-                if (isResumed() && mFadingStatus == IDLE
-                        && !mHandler.hasMessages(START_FADE_OUT)) {
+            if (isResumed() && getView().hasFocus()) {
+                showControlsOverlay(true);
+                if (enabled) {
+                    // StateGraph 7->2 5->2
                     startFadeTimer();
+                } else {
+                    // StateGraph 4->5 2->5
+                    stopFadeTimer();
                 }
             } else {
-                // Ensure fully opaque
-                mHandler.removeMessages(START_FADE_OUT);
-                fade(true);
+                // StateGraph 6->1 1->6
             }
         }
     }
 
     /**
-     * Returns true if view fading is enabled.
+     * Returns true if controls will be auto hidden after a delay when fragment is resumed.
      */
-    public boolean isFadingEnabled() {
+    public boolean isControlsOverlayAutoHideEnabled() {
         return mFadingEnabled;
     }
 
     /**
+     * @deprecated Uses {@link #setControlsOverlayAutoHideEnabled(boolean)}
+     */
+    @Deprecated
+    public void setFadingEnabled(boolean enabled) {
+        setControlsOverlayAutoHideEnabled(enabled);
+    }
+
+    /**
+     * @deprecated Uses {@link #isControlsOverlayAutoHideEnabled()}
+     */
+    @Deprecated
+    public boolean isFadingEnabled() {
+        return isControlsOverlayAutoHideEnabled();
+    }
+
+    /**
      * Sets the listener to be called when fade in or out has completed.
      * @hide
      */
@@ -348,46 +380,29 @@
     }
 
     /**
-     * Tickles the playback controls.  Fades in the view if it was faded out,
-     * otherwise resets the fade out timer.  Tickling on input events is handled
-     * by the fragment.
+     * Tickles the playback controls. Fades in the view if it was faded out. {@link #tickle()} will
+     * also kill the timer created by {@link #setControlsOverlayAutoHideEnabled(boolean)}. When
+     * next time fragment is resumed, the timer will be started again if
+     * {@link #isControlsOverlayAutoHideEnabled()} is true. In most cases app does not need call
+     * this method, tickling on input events is handled by the fragment.
      */
     public void tickle() {
         if (DEBUG) Log.v(TAG, "tickle enabled " + mFadingEnabled + " isResumed " + isResumed());
-        if (!mFadingEnabled || !isResumed()) {
-            return;
-        }
-        if (mHandler.hasMessages(START_FADE_OUT)) {
-            // Restart the timer
-            startFadeTimer();
-        } else {
-            fade(true);
-        }
-    }
-
-    /**
-     * Fades out the playback overlay immediately.
-     */
-    public void fadeOut() {
-        mHandler.removeMessages(START_FADE_OUT);
-        fade(false);
-    }
-
-    /**
-     * Returns true/false indicating whether playback controls are visible or not.
-     */
-    private boolean areControlsHidden() {
-        return mFadingStatus == IDLE && mBgAlpha == 0;
+        //StateGraph 2->4
+        stopFadeTimer();
+        showControlsOverlay(true);
     }
 
     private boolean onInterceptInputEvent(InputEvent event) {
-        final boolean controlsHidden = areControlsHidden();
+        final boolean controlsHidden = !mControlVisible;
         if (DEBUG) Log.v(TAG, "onInterceptInputEvent hidden " + controlsHidden + " " + event);
         boolean consumeEvent = false;
         int keyCode = KeyEvent.KEYCODE_UNKNOWN;
+        int keyAction = 0;
 
         if (event instanceof KeyEvent) {
             keyCode = ((KeyEvent) event).getKeyCode();
+            keyAction = ((KeyEvent) event).getAction();
             if (mInputEventHandler != null) {
                 consumeEvent = mInputEventHandler.onKey(getView(), keyCode, (KeyEvent) event);
             }
@@ -404,34 +419,60 @@
                 if (controlsHidden) {
                     consumeEvent = true;
                 }
-                tickle();
+                if (keyAction == KeyEvent.ACTION_DOWN) {
+                    tickle();
+                }
                 break;
             case KeyEvent.KEYCODE_BACK:
             case KeyEvent.KEYCODE_ESCAPE:
-                // If fading enabled and controls are not hidden, back will be consumed to fade
+                if (mInSeek) {
+                    // when in seek, the SeekUi will handle the BACK.
+                    return false;
+                }
+                // If controls are not hidden, back will be consumed to fade
                 // them out (even if the key was consumed by the handler).
-                if (mFadingEnabled && !controlsHidden) {
+                if (!controlsHidden) {
                     consumeEvent = true;
-                    mHandler.removeMessages(START_FADE_OUT);
-                    fade(false);
-                } else if (consumeEvent) {
-                    tickle();
+
+                    if (((KeyEvent) event).getAction() == KeyEvent.ACTION_UP) {
+                        hideControlsOverlay(true);
+                    }
                 }
                 break;
             default:
                 if (consumeEvent) {
-                    tickle();
+                    if (keyAction == KeyEvent.ACTION_DOWN) {
+                        tickle();
+                    }
                 }
         }
         return consumeEvent;
     }
 
     @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        // controls view are initially visible, make it invisible
+        // if app has called hideControlsOverlay() before view created.
+        mControlVisible = true;
+        if (!mControlVisibleBeforeOnCreateView) {
+            showControlsOverlay(false, false);
+            mControlVisibleBeforeOnCreateView = true;
+        }
+    }
+
+    @Override
     public void onResume() {
         super.onResume();
-        if (mFadingEnabled) {
-            setBgAlpha(0);
-            fade(true);
+
+        if (mControlVisible) {
+            //StateGraph: 6->5 1->2
+            if (mFadingEnabled) {
+                // StateGraph 1->2
+                startFadeTimer();
+            }
+        } else {
+            //StateGraph: 6->7 1->3
         }
         getVerticalGridView().setOnTouchInterceptListener(mOnTouchInterceptListener);
         getVerticalGridView().setOnKeyInterceptListener(mOnKeyInterceptListener);
@@ -440,6 +481,12 @@
         }
     }
 
+    private void stopFadeTimer() {
+        if (mHandler != null) {
+            mHandler.removeMessages(START_FADE_OUT);
+        }
+    }
+
     private void startFadeTimer() {
         if (mHandler != null) {
             mHandler.removeMessages(START_FADE_OUT);
@@ -474,31 +521,19 @@
     private TimeInterpolator mLogDecelerateInterpolator = new LogDecelerateInterpolator(100, 0);
     private TimeInterpolator mLogAccelerateInterpolator = new LogAccelerateInterpolator(100, 0);
 
-    private View getControlRowView() {
-        if (getVerticalGridView() == null) {
-            return null;
-        }
-        RecyclerView.ViewHolder vh = getVerticalGridView().findViewHolderForPosition(0);
-        if (vh == null) {
-            return null;
-        }
-        return vh.itemView;
-    }
-
     private void loadControlRowAnimator() {
-        final AnimatorListener listener = new AnimatorListener() {
-            @Override
-            void getViews(ArrayList<View> views) {
-                View view = getControlRowView();
-                if (view != null) {
-                    views.add(view);
-                }
-            }
-        };
         final AnimatorUpdateListener updateListener = new AnimatorUpdateListener() {
             @Override
             public void onAnimationUpdate(ValueAnimator arg0) {
-                View view = getControlRowView();
+                if (getVerticalGridView() == null) {
+                    return;
+                }
+                RecyclerView.ViewHolder vh = getVerticalGridView()
+                        .findViewHolderForAdapterPosition(0);
+                if (vh == null) {
+                    return;
+                }
+                View view = vh.itemView;
                 if (view != null) {
                     final float fraction = (Float) arg0.getAnimatedValue();
                     if (DEBUG) Log.v(TAG, "fraction " + fraction);
@@ -511,32 +546,15 @@
         Context context = getContext();
         mControlRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in);
         mControlRowFadeInAnimator.addUpdateListener(updateListener);
-        mControlRowFadeInAnimator.addListener(listener);
         mControlRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator);
 
         mControlRowFadeOutAnimator = loadAnimator(context,
                 R.animator.lb_playback_controls_fade_out);
         mControlRowFadeOutAnimator.addUpdateListener(updateListener);
-        mControlRowFadeOutAnimator.addListener(listener);
         mControlRowFadeOutAnimator.setInterpolator(mLogAccelerateInterpolator);
     }
 
     private void loadOtherRowAnimator() {
-        final AnimatorListener listener = new AnimatorListener() {
-            @Override
-            void getViews(ArrayList<View> views) {
-                if (getVerticalGridView() == null) {
-                    return;
-                }
-                final int count = getVerticalGridView().getChildCount();
-                for (int i = 0; i < count; i++) {
-                    View view = getVerticalGridView().getChildAt(i);
-                    if (view != null) {
-                        views.add(view);
-                    }
-                }
-            }
-        };
         final AnimatorUpdateListener updateListener = new AnimatorUpdateListener() {
             @Override
             public void onAnimationUpdate(ValueAnimator arg0) {
@@ -544,8 +562,10 @@
                     return;
                 }
                 final float fraction = (Float) arg0.getAnimatedValue();
-                for (View view : listener.mViews) {
-                    if (getVerticalGridView().getChildPosition(view) > 0) {
+                final int count = getVerticalGridView().getChildCount();
+                for (int i = 0; i < count; i++) {
+                    View view = getVerticalGridView().getChildAt(i);
+                    if (getVerticalGridView().getChildAdapterPosition(view) > 0) {
                         view.setAlpha(fraction);
                         view.setTranslationY((float) mAnimationTranslateY * (1f - fraction));
                     }
@@ -555,67 +575,133 @@
 
         Context context = getContext();
         mOtherRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in);
-        mOtherRowFadeInAnimator.addListener(listener);
         mOtherRowFadeInAnimator.addUpdateListener(updateListener);
         mOtherRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator);
 
         mOtherRowFadeOutAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_out);
-        mOtherRowFadeOutAnimator.addListener(listener);
         mOtherRowFadeOutAnimator.addUpdateListener(updateListener);
         mOtherRowFadeOutAnimator.setInterpolator(new AccelerateInterpolator());
     }
 
-    private void fade(boolean fadeIn) {
-        if (DEBUG) Log.v(TAG, "fade " + fadeIn);
-        if (getView() == null) {
-            return;
-        }
-        if ((fadeIn && mFadingStatus == IN) || (!fadeIn && mFadingStatus == OUT)) {
-            if (DEBUG) Log.v(TAG, "requested fade in progress");
-            return;
-        }
-        if ((fadeIn && mBgAlpha == 255) || (!fadeIn && mBgAlpha == 0)) {
-            if (DEBUG) Log.v(TAG, "fade is no-op");
-            return;
-        }
+    /**
+     * Fades out the playback overlay immediately.
+     * @deprecated Call {@link #hideControlsOverlay(boolean)}
+     */
+    @Deprecated
+    public void fadeOut() {
+        showControlsOverlay(false, false);
+    }
 
-        mAnimationTranslateY = getVerticalGridView().getSelectedPosition() == 0
-                ? mMajorFadeTranslateY : mMinorFadeTranslateY;
+    /**
+     * Show controls overlay.
+     *
+     * @param runAnimation True to run animation, false otherwise.
+     */
+    public void showControlsOverlay(boolean runAnimation) {
+        showControlsOverlay(true, runAnimation);
+    }
 
-        if (mFadingStatus == IDLE) {
-            if (fadeIn) {
-                mBgFadeInAnimator.start();
-                mControlRowFadeInAnimator.start();
-                mOtherRowFadeInAnimator.start();
-            } else {
-                mBgFadeOutAnimator.start();
-                mControlRowFadeOutAnimator.start();
-                mOtherRowFadeOutAnimator.start();
+    /**
+     * Returns true if controls overlay is visible, false otherwise.
+     *
+     * @return True if controls overlay is visible, false otherwise.
+     * @see #showControlsOverlay(boolean)
+     * @see #hideControlsOverlay(boolean)
+     */
+    public boolean isControlsOverlayVisible() {
+        return mControlVisible;
+    }
+
+    /**
+     * Hide controls overlay.
+     *
+     * @param runAnimation True to run animation, false otherwise.
+     */
+    public void hideControlsOverlay(boolean runAnimation) {
+        showControlsOverlay(false, runAnimation);
+    }
+
+    /**
+     * if first animator is still running, reverse it; otherwise start second animator.
+     */
+    static void reverseFirstOrStartSecond(ValueAnimator first, ValueAnimator second,
+            boolean runAnimation) {
+        if (first.isStarted()) {
+            first.reverse();
+            if (!runAnimation) {
+                first.end();
             }
         } else {
-            if (fadeIn) {
-                mBgFadeOutAnimator.reverse();
-                mControlRowFadeOutAnimator.reverse();
-                mOtherRowFadeOutAnimator.reverse();
-            } else {
-                mBgFadeInAnimator.reverse();
-                mControlRowFadeInAnimator.reverse();
-                mOtherRowFadeInAnimator.reverse();
+            second.start();
+            if (!runAnimation) {
+                second.end();
             }
         }
-        getView().announceForAccessibility(getString(fadeIn ? R.string.lb_playback_controls_shown
-                : R.string.lb_playback_controls_hidden));
+    }
 
-        // If fading in while control row is focused, set initial translationY so
-        // views slide in from below.
-        if (fadeIn && mFadingStatus == IDLE) {
-            final int count = getVerticalGridView().getChildCount();
-            for (int i = 0; i < count; i++) {
-                getVerticalGridView().getChildAt(i).setTranslationY(mAnimationTranslateY);
+    /**
+     * End first or second animator if they are still running.
+     */
+    static void endAll(ValueAnimator first, ValueAnimator second) {
+        if (first.isStarted()) {
+            first.end();
+        } else if (second.isStarted()) {
+            second.end();
+        }
+    }
+
+    /**
+     * Fade in or fade out rows and background.
+     *
+     * @param show True to fade in, false to fade out.
+     * @param animation True to run animation.
+     */
+    void showControlsOverlay(boolean show, boolean animation) {
+        if (DEBUG) Log.v(TAG, "showControlsOverlay " + show);
+        if (getView() == null) {
+            mControlVisibleBeforeOnCreateView = show;
+            return;
+        }
+        // force no animation when fragment is not resumed
+        if (!isResumed()) {
+            animation = false;
+        }
+        if (show == mControlVisible) {
+            if (!animation) {
+                // End animation if needed
+                endAll(mBgFadeInAnimator, mBgFadeOutAnimator);
+                endAll(mControlRowFadeInAnimator, mControlRowFadeOutAnimator);
+                endAll(mOtherRowFadeInAnimator, mOtherRowFadeOutAnimator);
             }
+            return;
+        }
+        // StateGraph: 7<->5 4<->3 2->3
+        mControlVisible = show;
+        if (!mControlVisible) {
+            // StateGraph 2->3
+            stopFadeTimer();
         }
 
-        mFadingStatus = fadeIn ? IN : OUT;
+        mAnimationTranslateY = (getVerticalGridView() == null
+                || getVerticalGridView().getSelectedPosition() == 0)
+                ? mMajorFadeTranslateY : mMinorFadeTranslateY;
+
+        if (show) {
+            reverseFirstOrStartSecond(mBgFadeOutAnimator, mBgFadeInAnimator, animation);
+            reverseFirstOrStartSecond(mControlRowFadeOutAnimator, mControlRowFadeInAnimator,
+                    animation);
+            reverseFirstOrStartSecond(mOtherRowFadeOutAnimator, mOtherRowFadeInAnimator, animation);
+        } else {
+            reverseFirstOrStartSecond(mBgFadeInAnimator, mBgFadeOutAnimator, animation);
+            reverseFirstOrStartSecond(mControlRowFadeInAnimator, mControlRowFadeOutAnimator,
+                    animation);
+            reverseFirstOrStartSecond(mOtherRowFadeInAnimator, mOtherRowFadeOutAnimator, animation);
+        }
+        if (animation) {
+            getView().announceForAccessibility(getString(show
+                    ? R.string.lb_playback_controls_shown
+                    : R.string.lb_playback_controls_hidden));
+        }
     }
 
     /**
@@ -714,7 +800,7 @@
     }
 
     private void updateBackground() {
-        if (mRootView != null) {
+        if (mBackgroundView != null) {
             int color = mBgDarkColor;
             switch (mBackgroundType) {
                 case BG_DARK:
@@ -726,7 +812,8 @@
                     color = Color.TRANSPARENT;
                     break;
             }
-            mRootView.setBackground(new ColorDrawable(color));
+            mBackgroundView.setBackground(new ColorDrawable(color));
+            setBgAlpha(mBgAlpha);
         }
     }
 
@@ -735,13 +822,21 @@
                 @Override
                 public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder vh) {
                     if (DEBUG) Log.v(TAG, "onAttachedToWindow " + vh.getViewHolder().view);
-                    if ((mFadingStatus == IDLE && mBgAlpha == 0) || mFadingStatus == OUT) {
+                    if (!mControlVisible) {
                         if (DEBUG) Log.v(TAG, "setting alpha to 0");
                         vh.getViewHolder().view.setAlpha(0);
                     }
                 }
 
                 @Override
+                public void onCreate(ItemBridgeAdapter.ViewHolder vh) {
+                    Presenter.ViewHolder viewHolder = vh.getViewHolder();
+                    if (viewHolder instanceof PlaybackSeekUi) {
+                        ((PlaybackSeekUi) viewHolder).setPlaybackSeekUiClient(mChainedClient);
+                    }
+                }
+
+                @Override
                 public void onDetachedFromWindow(ItemBridgeAdapter.ViewHolder vh) {
                     if (DEBUG) Log.v(TAG, "onDetachedFromWindow " + vh.getViewHolder().view);
                     // Reset animation state
@@ -759,6 +854,7 @@
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
                              Bundle savedInstanceState) {
         mRootView = inflater.inflate(R.layout.lb_playback_fragment, container, false);
+        mBackgroundView = mRootView.findViewById(R.id.playback_fragment_background);
         mRowsSupportFragment = (RowsSupportFragment) getChildFragmentManager().findFragmentById(
                 R.id.playback_controls_dock);
         if (mRowsSupportFragment == null) {
@@ -778,6 +874,10 @@
         mBgAlpha = 255;
         updateBackground();
         mRowsSupportFragment.setExternalAdapterListener(mAdapterListener);
+        ProgressBarManager progressBarManager = getProgressBarManager();
+        if (progressBarManager != null) {
+            progressBarManager.setRootView((ViewGroup) mRootView);
+        }
         return mRootView;
     }
 
@@ -812,6 +912,12 @@
         if (mHostCallback != null) {
             mHostCallback.onHostPause();
         }
+        if (mHandler.hasMessages(START_FADE_OUT)) {
+            // StateGraph: 2->1
+            mHandler.removeMessages(START_FADE_OUT);
+        } else {
+            // StateGraph: 5->6, 7->6, 4->1, 3->1
+        }
         super.onPause();
     }
 
@@ -842,6 +948,7 @@
     @Override
     public void onDestroyView() {
         mRootView = null;
+        mBackgroundView = null;
         super.onDestroyView();
     }
 
@@ -955,36 +1062,111 @@
         }
     }
 
-    static abstract class AnimatorListener implements Animator.AnimatorListener {
-        ArrayList<View> mViews = new ArrayList<View>();
-        ArrayList<Integer> mLayerType = new ArrayList<Integer>();
-
+    final PlaybackSeekUi.Client mChainedClient = new PlaybackSeekUi.Client() {
         @Override
-        public void onAnimationCancel(Animator animation) {
+        public boolean isSeekEnabled() {
+            return mSeekUiClient == null ? false : mSeekUiClient.isSeekEnabled();
         }
 
         @Override
-        public void onAnimationRepeat(Animator animation) {
+        public void onSeekStarted() {
+            if (mSeekUiClient != null) {
+                mSeekUiClient.onSeekStarted();
+            }
+            setSeekMode(true);
         }
 
         @Override
-        public void onAnimationStart(Animator animation) {
-            getViews(mViews);
-            for (View view : mViews) {
-                mLayerType.add(view.getLayerType());
-                view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+        public PlaybackSeekDataProvider getPlaybackSeekDataProvider() {
+            return mSeekUiClient == null ? null : mSeekUiClient.getPlaybackSeekDataProvider();
+        }
+
+        @Override
+        public void onSeekPositionChanged(long pos) {
+            if (mSeekUiClient != null) {
+                mSeekUiClient.onSeekPositionChanged(pos);
             }
         }
 
         @Override
-        public void onAnimationEnd(Animator animation) {
-            for (int i = 0; i < mViews.size(); i++) {
-                mViews.get(i).setLayerType(mLayerType.get(i), null);
+        public void onSeekFinished(boolean cancelled) {
+            if (mSeekUiClient != null) {
+                mSeekUiClient.onSeekFinished(cancelled);
             }
-            mLayerType.clear();
-            mViews.clear();
+            setSeekMode(false);
         }
+    };
 
-        abstract void getViews(ArrayList<View> views);
+    /**
+     * Interface to be implemented by UI widget to support PlaybackSeekUi.
+     */
+    public void setPlaybackSeekUiClient(PlaybackSeekUi.Client client) {
+        mSeekUiClient = client;
+    }
+
+    /**
+     * Show or hide other rows other than PlaybackRow.
+     * @param inSeek True to make other rows visible, false to make other rows invisible.
+     */
+    void setSeekMode(boolean inSeek) {
+        if (mInSeek == inSeek) {
+            return;
+        }
+        mInSeek = inSeek;
+        getVerticalGridView().setSelectedPosition(0);
+        if (mInSeek) {
+            stopFadeTimer();
+        }
+        // immediately fade in control row.
+        showControlsOverlay(true);
+        final int count = getVerticalGridView().getChildCount();
+        for (int i = 0; i < count; i++) {
+            View view = getVerticalGridView().getChildAt(i);
+            if (getVerticalGridView().getChildAdapterPosition(view) > 0) {
+                view.setVisibility(mInSeek ? View.INVISIBLE : View.VISIBLE);
+            }
+        }
+    }
+
+    /**
+     * Called when size of the video changes. App may override.
+     * @param videoWidth Intrinsic width of video
+     * @param videoHeight Intrinsic height of video
+     */
+    protected void onVideoSizeChanged(int videoWidth, int videoHeight) {
+    }
+
+    /**
+     * Called when media has start or stop buffering. App may override. The default initial state
+     * is not buffering.
+     * @param start True for buffering start, false otherwise.
+     */
+    protected void onBufferingStateChanged(boolean start) {
+        ProgressBarManager progressBarManager = getProgressBarManager();
+        if (progressBarManager != null) {
+            if (start) {
+                progressBarManager.show();
+            } else {
+                progressBarManager.hide();
+            }
+        }
+    }
+
+    /**
+     * Called when media has error. App may override.
+     * @param errorCode Optional error code for specific implementation.
+     * @param errorMessage Optional error message for specific implementation.
+     */
+    protected void onError(int errorCode, CharSequence errorMessage) {
+    }
+
+    /**
+     * Returns the ProgressBarManager that will show or hide progress bar in
+     * {@link #onBufferingStateChanged(boolean)}.
+     * @return The ProgressBarManager that will show or hide progress bar in
+     * {@link #onBufferingStateChanged(boolean)}.
+     */
+    public ProgressBarManager getProgressBarManager() {
+        return mProgressBarManager;
     }
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragmentGlueHost.java b/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragmentGlueHost.java
index da644ae..cdf3f97 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragmentGlueHost.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragmentGlueHost.java
@@ -21,6 +21,7 @@
 import android.support.v17.leanback.widget.OnActionClickedListener;
 import android.support.v17.leanback.widget.OnItemViewClickedListener;
 import android.support.v17.leanback.widget.PlaybackRowPresenter;
+import android.support.v17.leanback.widget.PlaybackSeekUi;
 import android.support.v17.leanback.widget.Presenter;
 import android.support.v17.leanback.widget.Row;
 import android.support.v17.leanback.widget.RowPresenter;
@@ -30,7 +31,7 @@
  * {@link PlaybackGlueHost} implementation
  * the interaction between this class and {@link PlaybackSupportFragment}.
  */
-public class PlaybackSupportFragmentGlueHost extends PlaybackGlueHost {
+public class PlaybackSupportFragmentGlueHost extends PlaybackGlueHost implements PlaybackSeekUi {
     private final PlaybackSupportFragment mFragment;
 
     public PlaybackSupportFragmentGlueHost(PlaybackSupportFragment fragment) {
@@ -38,8 +39,13 @@
     }
 
     @Override
-    public void setFadingEnabled(boolean enable) {
-        mFragment.setFadingEnabled(enable);
+    public void setControlsOverlayAutoHideEnabled(boolean enabled) {
+        mFragment.setControlsOverlayAutoHideEnabled(enabled);
+    }
+
+    @Override
+    public boolean isControlsOverlayAutoHideEnabled() {
+        return mFragment.isControlsOverlayAutoHideEnabled();
     }
 
     @Override
@@ -88,4 +94,47 @@
     public void fadeOut() {
         mFragment.fadeOut();
     }
+
+    @Override
+    public boolean isControlsOverlayVisible() {
+        return mFragment.isControlsOverlayVisible();
+    }
+
+    @Override
+    public void hideControlsOverlay(boolean runAnimation) {
+        mFragment.hideControlsOverlay(runAnimation);
+    }
+
+    @Override
+    public void showControlsOverlay(boolean runAnimation) {
+        mFragment.showControlsOverlay(runAnimation);
+    }
+
+    @Override
+    public void setPlaybackSeekUiClient(Client client) {
+        mFragment.setPlaybackSeekUiClient(client);
+    }
+
+    final PlayerCallback mPlayerCallback =
+            new PlayerCallback() {
+                @Override
+                public void onBufferingStateChanged(boolean start) {
+                    mFragment.onBufferingStateChanged(start);
+                }
+
+                @Override
+                public void onError(int errorCode, CharSequence errorMessage) {
+                    mFragment.onError(errorCode, errorMessage);
+                }
+
+                @Override
+                public void onVideoSizeChanged(int videoWidth, int videoHeight) {
+                    mFragment.onVideoSizeChanged(videoWidth, videoHeight);
+                }
+            };
+
+    @Override
+    public PlayerCallback getPlayerCallback() {
+        return mPlayerCallback;
+    }
 }
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 150e461..41241d0 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/VideoFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/VideoFragment.java
@@ -83,6 +83,24 @@
         }
     }
 
+    @Override
+    protected void onVideoSizeChanged(int width, int height) {
+        int screenWidth = getView().getWidth();
+        int screenHeight = getView().getHeight();
+
+        ViewGroup.LayoutParams p = mVideoSurface.getLayoutParams();
+        if (screenWidth * height > width * screenHeight) {
+            // fit in screen height
+            p.height = screenHeight;
+            p.width = screenHeight * width / height;
+        } else {
+            // fit in screen width
+            p.width = screenWidth;
+            p.height = screenWidth * height / width;
+        }
+        mVideoSurface.setLayoutParams(p);
+    }
+
     /**
      * Returns the surface view.
      */
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VideoFragmentGlueHost.java b/v17/leanback/src/android/support/v17/leanback/app/VideoFragmentGlueHost.java
index ee9a536..a64b521 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/VideoFragmentGlueHost.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/VideoFragmentGlueHost.java
@@ -40,4 +40,5 @@
     public void setSurfaceHolderCallback(SurfaceHolder.Callback callback) {
         mFragment.setSurfaceHolderCallback(callback);
     }
+
 }
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 c12b06f..321bdbe 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragment.java
@@ -86,6 +86,24 @@
         }
     }
 
+    @Override
+    protected void onVideoSizeChanged(int width, int height) {
+        int screenWidth = getView().getWidth();
+        int screenHeight = getView().getHeight();
+
+        ViewGroup.LayoutParams p = mVideoSurface.getLayoutParams();
+        if (screenWidth * height > width * screenHeight) {
+            // fit in screen height
+            p.height = screenHeight;
+            p.width = screenHeight * width / height;
+        } else {
+            // fit in screen width
+            p.width = screenWidth;
+            p.height = screenWidth * height / width;
+        }
+        mVideoSurface.setLayoutParams(p);
+    }
+
     /**
      * Returns the surface view.
      */
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragmentGlueHost.java b/v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragmentGlueHost.java
index 67150a0..28f919b 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragmentGlueHost.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragmentGlueHost.java
@@ -43,4 +43,5 @@
     public void setSurfaceHolderCallback(SurfaceHolder.Callback callback) {
         mFragment.setSurfaceHolderCallback(callback);
     }
+
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/media/MediaPlayerAdapter.java b/v17/leanback/src/android/support/v17/leanback/media/MediaPlayerAdapter.java
new file mode 100644
index 0000000..3de7aa1
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/media/MediaPlayerAdapter.java
@@ -0,0 +1,395 @@
+/*
+ * 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 android.content.Context;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Handler;
+import android.support.v17.leanback.R;
+import android.view.SurfaceHolder;
+
+import java.io.IOException;
+
+/**
+ * This implementation extends the {@link PlayerAdapter} with a {@link MediaPlayer}.
+ */
+public class MediaPlayerAdapter extends PlayerAdapter {
+
+    Context mContext;
+    final MediaPlayer mPlayer = new MediaPlayer();
+    SurfaceHolderGlueHost mSurfaceHolderGlueHost;
+    final Runnable mRunnable = new Runnable() {
+        @Override
+        public void run() {
+            getCallback().onCurrentPositionChanged(MediaPlayerAdapter.this);
+            mHandler.postDelayed(this, getUpdatePeriod());
+        }
+    };;
+    final Handler mHandler = new Handler();
+    boolean mInitialized = false; // true when the MediaPlayer is prepared/initialized
+    Uri mMediaSourceUri = null;
+    boolean mHasDisplay;
+    long mBufferedProgress;
+
+    MediaPlayer.OnPreparedListener mOnPreparedListener = new MediaPlayer.OnPreparedListener() {
+        @Override
+        public void onPrepared(MediaPlayer mp) {
+            mInitialized = true;
+            notifyBufferingStartEnd();
+            if (mSurfaceHolderGlueHost == null || mHasDisplay) {
+                getCallback().onPreparedStateChanged(MediaPlayerAdapter.this);
+            }
+        }
+    };
+
+    final MediaPlayer.OnCompletionListener mOnCompletionListener =
+            new MediaPlayer.OnCompletionListener() {
+        @Override
+        public void onCompletion(MediaPlayer mediaPlayer) {
+            getCallback().onPlayStateChanged(MediaPlayerAdapter.this);
+            getCallback().onPlayCompleted(MediaPlayerAdapter.this);
+        }
+    };
+
+    final MediaPlayer.OnBufferingUpdateListener mOnBufferingUpdateListener =
+            new MediaPlayer.OnBufferingUpdateListener() {
+        @Override
+        public void onBufferingUpdate(MediaPlayer mp, int percent) {
+            mBufferedProgress = getDuration() * percent / 100;
+            getCallback().onBufferedPositionChanged(MediaPlayerAdapter.this);
+        }
+    };
+
+    final MediaPlayer.OnVideoSizeChangedListener mOnVideoSizeChangedListener =
+            new MediaPlayer.OnVideoSizeChangedListener() {
+        @Override
+        public void onVideoSizeChanged(MediaPlayer mediaPlayer, int width, int height) {
+            getCallback().onVideoSizeChanged(MediaPlayerAdapter.this, width, height);
+        }
+    };
+
+    final MediaPlayer.OnErrorListener mOnErrorListener =
+            new MediaPlayer.OnErrorListener() {
+                @Override
+                public boolean onError(MediaPlayer mp, int what, int extra) {
+                    getCallback().onError(MediaPlayerAdapter.this, what,
+                            mContext.getString(R.string.lb_media_player_error, what, extra));
+                    return MediaPlayerAdapter.this.onError(what, extra);
+                }
+            };
+
+    final MediaPlayer.OnSeekCompleteListener mOnSeekCompleteListener =
+            new MediaPlayer.OnSeekCompleteListener() {
+                @Override
+                public void onSeekComplete(MediaPlayer mp) {
+                    MediaPlayerAdapter.this.onSeekComplete();
+                }
+            };
+
+    final MediaPlayer.OnInfoListener mOnInfoListener = new MediaPlayer.OnInfoListener() {
+        @Override
+        public boolean onInfo(MediaPlayer mp, int what, int extra) {
+            boolean handled = false;
+            switch (what) {
+                case MediaPlayer.MEDIA_INFO_BUFFERING_START:
+                    mBufferingStart = true;
+                    notifyBufferingStartEnd();
+                    handled = true;
+                    break;
+                case MediaPlayer.MEDIA_INFO_BUFFERING_END:
+                    mBufferingStart = false;
+                    notifyBufferingStartEnd();
+                    handled = true;
+                    break;
+            }
+            boolean thisHandled = MediaPlayerAdapter.this.onInfo(what, extra);
+            return handled || thisHandled;
+        }
+    };
+
+    boolean mBufferingStart;
+
+    void notifyBufferingStartEnd() {
+        getCallback().onBufferingStateChanged(MediaPlayerAdapter.this,
+                mBufferingStart || !mInitialized);
+    }
+
+    /**
+     * Constructor.
+     */
+    public MediaPlayerAdapter(Context context) {
+        mContext = context;
+    }
+
+    @Override
+    public void onAttachedToHost(PlaybackGlueHost host) {
+        if (host instanceof SurfaceHolderGlueHost) {
+            mSurfaceHolderGlueHost = ((SurfaceHolderGlueHost) host);
+            mSurfaceHolderGlueHost.setSurfaceHolderCallback(new VideoPlayerSurfaceHolderCallback());
+        }
+    }
+
+    /**
+     * Will reset the {@link MediaPlayer} and the glue such that a new file can be played. You are
+     * not required to call this method before playing the first file. However you have to call it
+     * before playing a second one.
+     */
+    public void reset() {
+        changeToUnitialized();
+        mPlayer.reset();
+    }
+
+    void changeToUnitialized() {
+        if (mInitialized) {
+            mInitialized = false;
+            notifyBufferingStartEnd();
+            if (mHasDisplay) {
+                getCallback().onPreparedStateChanged(MediaPlayerAdapter.this);
+            }
+        }
+    }
+
+    /**
+     * Release internal MediaPlayer. Should not use the object after call release().
+     */
+    public void release() {
+        changeToUnitialized();
+        mHasDisplay = false;
+        mPlayer.release();
+    }
+
+    @Override
+    public void onDetachedFromHost() {
+        if (mSurfaceHolderGlueHost != null) {
+            mSurfaceHolderGlueHost.setSurfaceHolderCallback(null);
+            mSurfaceHolderGlueHost = null;
+        }
+        reset();
+        release();
+    }
+
+    /**
+     * Called to indicate an error.
+     *
+     * @param what    the type of error that has occurred:
+     * <ul>
+     * <li>{@link MediaPlayer#MEDIA_ERROR_UNKNOWN}
+     * <li>{@link MediaPlayer#MEDIA_ERROR_SERVER_DIED}
+     * </ul>
+     * @param extra an extra code, specific to the error. Typically
+     * implementation dependent.
+     * <ul>
+     * <li>{@link MediaPlayer#MEDIA_ERROR_IO}
+     * <li>{@link MediaPlayer#MEDIA_ERROR_MALFORMED}
+     * <li>{@link MediaPlayer#MEDIA_ERROR_UNSUPPORTED}
+     * <li>{@link MediaPlayer#MEDIA_ERROR_TIMED_OUT}
+     * <li><code>MEDIA_ERROR_SYSTEM (-2147483648)</code> - low-level system error.
+     * </ul>
+     * @return True if the method handled the error, false if it didn't.
+     * Returning false, will cause the {@link PlayerAdapter.Callback#onPlayCompleted(PlayerAdapter)}
+     * being called.
+     */
+    protected boolean onError(int what, int extra) {
+        return false;
+    }
+
+    /**
+     * Called to indicate the completion of a seek operation.
+     */
+    protected void onSeekComplete() {
+    }
+
+    /**
+     * Called to indicate an info or a warning.
+     *
+     * @param what    the type of info or warning.
+     * <ul>
+     * <li>{@link MediaPlayer#MEDIA_INFO_UNKNOWN}
+     * <li>{@link MediaPlayer#MEDIA_INFO_VIDEO_TRACK_LAGGING}
+     * <li>{@link MediaPlayer#MEDIA_INFO_VIDEO_RENDERING_START}
+     * <li>{@link MediaPlayer#MEDIA_INFO_BUFFERING_START}
+     * <li>{@link MediaPlayer#MEDIA_INFO_BUFFERING_END}
+     * <li><code>MEDIA_INFO_NETWORK_BANDWIDTH (703)</code> -
+     *     bandwidth information is available (as <code>extra</code> kbps)
+     * <li>{@link MediaPlayer#MEDIA_INFO_BAD_INTERLEAVING}
+     * <li>{@link MediaPlayer#MEDIA_INFO_NOT_SEEKABLE}
+     * <li>{@link MediaPlayer#MEDIA_INFO_METADATA_UPDATE}
+     * <li>{@link MediaPlayer#MEDIA_INFO_UNSUPPORTED_SUBTITLE}
+     * <li>{@link MediaPlayer#MEDIA_INFO_SUBTITLE_TIMED_OUT}
+     * </ul>
+     * @param extra an extra code, specific to the info. Typically
+     * implementation dependent.
+     * @return True if the method handled the info, false if it didn't.
+     * Returning false, will cause the info to be discarded.
+     */
+    protected boolean onInfo(int what, int extra) {
+        return false;
+    }
+
+    /**
+     * @see MediaPlayer#setDisplay(SurfaceHolder)
+     */
+    void setDisplay(SurfaceHolder surfaceHolder) {
+        boolean hadDisplay = mHasDisplay;
+        mHasDisplay = surfaceHolder != null;
+        if (hadDisplay == mHasDisplay) {
+            return;
+        }
+        mPlayer.setDisplay(surfaceHolder);
+        if (mHasDisplay) {
+            if (mInitialized) {
+                getCallback().onPreparedStateChanged(MediaPlayerAdapter.this);
+            }
+        } else {
+            if (mInitialized) {
+                getCallback().onPreparedStateChanged(MediaPlayerAdapter.this);
+            }
+        }
+
+    }
+
+    @Override
+    public void setProgressUpdatingEnabled(final boolean enabled) {
+        mHandler.removeCallbacks(mRunnable);
+        if (!enabled) {
+            return;
+        }
+        mHandler.postDelayed(mRunnable, getUpdatePeriod());
+    }
+
+    int getUpdatePeriod() {
+        return 16;
+    }
+
+    @Override
+    public boolean isPlaying() {
+        return mInitialized && mPlayer.isPlaying();
+    }
+
+    @Override
+    public long getDuration() {
+        return mInitialized ? mPlayer.getDuration() : -1;
+    }
+
+    @Override
+    public long getCurrentPosition() {
+        return mInitialized ? mPlayer.getCurrentPosition() : -1;
+    }
+
+    @Override
+    public void play() {
+        if (!mInitialized || mPlayer.isPlaying()) {
+            return;
+        }
+        mPlayer.start();
+        getCallback().onPlayStateChanged(MediaPlayerAdapter.this);
+        getCallback().onCurrentPositionChanged(MediaPlayerAdapter.this);
+    }
+
+    @Override
+    public void pause() {
+        if (isPlaying()) {
+            mPlayer.pause();
+            getCallback().onPlayStateChanged(MediaPlayerAdapter.this);
+        }
+    }
+
+    @Override
+    public void seekTo(long newPosition) {
+        if (!mInitialized) {
+            return;
+        }
+        mPlayer.seekTo((int) newPosition);
+    }
+
+    @Override
+    public long getBufferedPosition() {
+        return mBufferedProgress;
+    }
+
+    /**
+     * Sets the media source of the player witha given URI.
+     *
+     * @return Returns <code>true</code> if uri represents a new media; <code>false</code>
+     * otherwise.
+     * @see MediaPlayer#setDataSource(String)
+     */
+    public boolean setDataSource(Uri uri) {
+        if (mMediaSourceUri != null ? mMediaSourceUri.equals(uri) : uri == null) {
+            return false;
+        }
+        mMediaSourceUri = uri;
+        prepareMediaForPlaying();
+        return true;
+    }
+
+    private void prepareMediaForPlaying() {
+        reset();
+        try {
+            if (mMediaSourceUri != null) {
+                mPlayer.setDataSource(mContext, mMediaSourceUri);
+            } else {
+                return;
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new RuntimeException(e);
+        }
+        mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
+        mPlayer.setOnPreparedListener(mOnPreparedListener);
+        mPlayer.setOnVideoSizeChangedListener(mOnVideoSizeChangedListener);
+        mPlayer.setOnErrorListener(mOnErrorListener);
+        mPlayer.setOnSeekCompleteListener(mOnSeekCompleteListener);
+        mPlayer.setOnCompletionListener(mOnCompletionListener);
+        mPlayer.setOnInfoListener(mOnInfoListener);
+        mPlayer.setOnBufferingUpdateListener(mOnBufferingUpdateListener);
+        notifyBufferingStartEnd();
+        mPlayer.prepareAsync();
+        getCallback().onPlayStateChanged(MediaPlayerAdapter.this);
+    }
+
+    /**
+     * @return True if MediaPlayer OnPreparedListener is invoked and got a SurfaceHolder if
+     * {@link PlaybackGlueHost} provides SurfaceHolder.
+     */
+    @Override
+    public boolean isPrepared() {
+        return mInitialized && (mSurfaceHolderGlueHost == null || mHasDisplay);
+    }
+
+    /**
+     * Implements {@link SurfaceHolder.Callback} that can then be set on the
+     * {@link PlaybackGlueHost}.
+     */
+    class VideoPlayerSurfaceHolderCallback implements SurfaceHolder.Callback {
+        @Override
+        public void surfaceCreated(SurfaceHolder surfaceHolder) {
+            setDisplay(surfaceHolder);
+        }
+
+        @Override
+        public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
+        }
+
+        @Override
+        public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
+            setDisplay(null);
+        }
+    }
+}
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 204f922..886b587 100644
--- a/v17/leanback/src/android/support/v17/leanback/media/MediaPlayerGlue.java
+++ b/v17/leanback/src/android/support/v17/leanback/media/MediaPlayerGlue.java
@@ -34,6 +34,7 @@
 import android.view.View;
 
 import java.io.IOException;
+import java.util.List;
 
 /**
  * This glue extends the {@link android.support.v17.leanback.media.PlaybackControlGlue} with a
@@ -71,7 +72,6 @@
     private long mLastKeyDownEvent = 0L; // timestamp when the last DPAD_CENTER KEY_DOWN occurred
     private Uri mMediaSourceUri = null;
     private String mMediaSourcePath = null;
-    private PlayerCallback mPlayerCallback;
     private MediaPlayer.OnCompletionListener mOnCompletionListener;
     private String mArtist;
     private String mTitle;
@@ -138,28 +138,32 @@
     }
 
     /**
-     * Sets the callback, which would tell the listener that video is ready to be played.
-     */
-    @Override
-    public void setPlayerCallback(PlayerCallback callback) {
-        this.mPlayerCallback = callback;
-    }
-
-    /**
      * Will reset the {@link MediaPlayer} and the glue such that a new file can be played. You are
      * not required to call this method before playing the first file. However you have to call it
      * before playing a second one.
      */
     public void reset() {
-        mInitialized = false;
+        changeToUnitialized();
         mPlayer.reset();
     }
 
+    void changeToUnitialized() {
+        if (mInitialized) {
+            mInitialized = false;
+            List<PlayerCallback> callbacks = getPlayerCallbacks();
+            if (callbacks != null) {
+                for (PlayerCallback callback: callbacks) {
+                    callback.onPreparedStateChanged(MediaPlayerGlue.this);
+                }
+            }
+        }
+    }
+
     /**
      * Release internal MediaPlayer. Should not use the object after call release().
      */
     public void release() {
-        mInitialized = false;
+        changeToUnitialized();
         mPlayer.release();
     }
 
@@ -271,6 +275,11 @@
     }
 
     @Override
+    public boolean isPlaying() {
+        return isMediaPlaying();
+    }
+
+    @Override
     public CharSequence getMediaTitle() {
         return mTitle != null ? mTitle : "N/a";
     }
@@ -427,8 +436,11 @@
             @Override
             public void onPrepared(MediaPlayer mp) {
                 mInitialized = true;
-                if (mPlayerCallback != null) {
-                    mPlayerCallback.onReadyForPlayback();
+                List<PlayerCallback> callbacks = getPlayerCallbacks();
+                if (callbacks != null) {
+                    for (PlayerCallback callback: callbacks) {
+                        callback.onPreparedStateChanged(MediaPlayerGlue.this);
+                    }
                 }
             }
         });
@@ -477,6 +489,11 @@
         return mInitialized;
     }
 
+    @Override
+    public boolean isPrepared() {
+        return mInitialized;
+    }
+
     /**
      * Implements {@link SurfaceHolder.Callback} that can then be set on the
      * {@link PlaybackGlueHost}.
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 945999d..6363479 100644
--- a/v17/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java
+++ b/v17/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java
@@ -36,6 +36,7 @@
 import android.view.View;
 
 import java.lang.ref.WeakReference;
+import java.util.List;
 
 /**
  * A helper class for managing a {@link PlaybackControlsRow}
@@ -324,7 +325,7 @@
     public void setFadingEnabled(boolean enable) {
         mFadeWhenPlaying = enable;
         if (!mFadeWhenPlaying && getHost() != null) {
-            getHost().setFadingEnabled(false);
+            getHost().setControlsOverlayAutoHideEnabled(false);
         }
     }
 
@@ -701,7 +702,7 @@
         }
 
         if (mFadeWhenPlaying && getHost() != null) {
-            getHost().setFadingEnabled(playbackSpeed == PLAYBACK_SPEED_NORMAL);
+            getHost().setControlsOverlayAutoHideEnabled(playbackSpeed == PLAYBACK_SPEED_NORMAL);
         }
 
         if (mPlayPauseAction != null) {
@@ -713,6 +714,12 @@
                 notifyItemChanged(primaryActionsAdapter, mPlayPauseAction);
             }
         }
+        List<PlayerCallback> callbacks = getPlayerCallbacks();
+        if (callbacks != null) {
+            for (int i = 0, size = callbacks.size(); i < size; i++) {
+                callbacks.get(i).onPlayStateChanged(this);
+            }
+        }
     }
 
     private static void notifyItemChanged(SparseArrayObjectAdapter adapter, Object object) {
@@ -764,6 +771,11 @@
      */
     public abstract boolean isMediaPlaying();
 
+    @Override
+    public boolean isPlaying() {
+        return isMediaPlaying();
+    }
+
     /**
      * Returns the title of the media item.
      */
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 3f55da3..32d5545 100644
--- a/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlue.java
+++ b/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlue.java
@@ -19,6 +19,9 @@
 import android.content.Context;
 import android.support.annotation.CallSuper;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * Base class for abstraction of media play/pause feature. A subclass of PlaybackGlue will contain
  * implementation of Media Player or a connection to playback Service. App initializes
@@ -42,15 +45,44 @@
     private PlaybackGlueHost mPlaybackGlueHost;
 
     /**
-     * Interface to allow clients to take action once the video is ready to play.
+     * Interface to allow clients to take action once the video is ready to play and start stop.
      */
     public abstract static class PlayerCallback {
         /**
-         * This method is fired when the video is ready for playback.
+         * This method is fired when media is ready for playback {@link #isPrepared()}.
+         * @deprecated use {@link #onPreparedStateChanged(PlaybackGlue)}.
          */
-        public abstract void onReadyForPlayback();
+        @Deprecated
+        public void onReadyForPlayback() {
+        }
+
+        /**
+         * Event for {@link #isPrepared()} changed.
+         * @param glue The PlaybackGlue that has changed {@link #isPrepared()}.
+         */
+        public void onPreparedStateChanged(PlaybackGlue glue) {
+            if (glue.isPrepared()) {
+                onReadyForPlayback();
+            }
+        }
+
+        /**
+         * Event for Play/Pause state change. See {@link #isPlaying()}}.
+         * @param glue The PlaybackGlue that has changed playing or pausing state.
+         */
+        public void onPlayStateChanged(PlaybackGlue glue) {
+        }
+
+        /**
+         * Event of the current media is finished.
+         * @param glue The PlaybackGlue that has finished current media playing.
+         */
+        public void onPlayCompleted(PlaybackGlue glue) {
+        }
     }
 
+    ArrayList<PlayerCallback> mPlayerCallbacks;
+
     /**
      * Constructor.
      */
@@ -71,15 +103,74 @@
      * {@link PlayerCallback#onReadyForPlayback()} event.
      *
      * @see PlayerCallback#onReadyForPlayback()
+     * @deprecated Use isPrepared() instead.
      */
+    @Deprecated
     public boolean isReadyForPlayback() {
         return true;
     }
 
     /**
-     * Sets the {@link PlayerCallback} callback.
+     * Returns true when the media player is prepared to start media playback. When returning false,
+     * app may listen to {@link PlayerCallback#onPreparedStateChanged(PlaybackGlue)} event.
+     * @return True if prepared, false otherwise.
      */
+    public boolean isPrepared() {
+        return isReadyForPlayback();
+    }
+
+    /**
+     * Sets the {@link PlayerCallback} callback. It will reset the existing callbacks.
+     * In most cases you would call {@link #addPlayerCallback(PlayerCallback)}.
+     * @deprecated Use {@link #addPlayerCallback(PlayerCallback)}.
+     */
+    @Deprecated
     public void setPlayerCallback(PlayerCallback playerCallback) {
+        if (playerCallback == null) {
+            if (mPlayerCallbacks != null) {
+                mPlayerCallbacks.clear();
+            }
+        } else {
+            addPlayerCallback(playerCallback);
+        }
+    }
+
+    /**
+     * Add a PlayerCallback.
+     * @param playerCallback The callback to add.
+     */
+    public void addPlayerCallback(PlayerCallback playerCallback) {
+        if (mPlayerCallbacks == null) {
+            mPlayerCallbacks = new ArrayList();
+        }
+        mPlayerCallbacks.add(playerCallback);
+    }
+
+    /**
+     * Remove a PlayerCallback.
+     * @param callback The callback to remove.
+     */
+    public void removePlayerCallback(PlayerCallback callback) {
+        if (mPlayerCallbacks != null) {
+            mPlayerCallbacks.remove(callback);
+        }
+    }
+
+    /**
+     * @return A snapshot of list of PlayerCallbacks set on the Glue.
+     */
+    protected List<PlayerCallback> getPlayerCallbacks() {
+        if (mPlayerCallbacks == null) {
+            return null;
+        }
+        return new ArrayList(mPlayerCallbacks);
+    }
+
+    /**
+     * Returns true if media is currently playing.
+     */
+    public boolean isPlaying() {
+        return false;
     }
 
     /**
@@ -179,9 +270,7 @@
 
             @Override
             public void onHostDestroy() {
-                if (mPlaybackGlueHost != null) {
-                    mPlaybackGlueHost.attachToGlue(null);
-                }
+                setHost(null);
             }
         });
     }
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 799074c..8985b5d 100644
--- a/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlueHost.java
+++ b/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlueHost.java
@@ -18,6 +18,7 @@
 
 import android.support.v17.leanback.widget.OnActionClickedListener;
 import android.support.v17.leanback.widget.PlaybackRowPresenter;
+import android.support.v17.leanback.widget.PlaybackSeekUi;
 import android.support.v17.leanback.widget.Row;
 import android.view.View;
 
@@ -28,7 +29,7 @@
  * <li>Render UI of PlaybackGlue: {@link #setPlaybackRow(Row)},
  * {@link #setPlaybackRowPresenter(PlaybackRowPresenter)}.
  * </li>
- * <li>Callback for fragment/activity onStart/onStop: {@link #setHostCallback(HostCallback)}.
+ * <li>Client for fragment/activity onStart/onStop: {@link #setHostCallback(HostCallback)}.
  * </li>
  * <li>Auto fade out controls after a short period: {@link #setFadingEnabled(boolean)}.
  * </li>
@@ -36,9 +37,11 @@
  * {@link #setOnActionClickedListener(OnActionClickedListener)}.
  * </li>
  *
- * Subclass of PlaybackGlueHost may implement optional interface e.g. {@link SurfaceHolderGlueHost}
- * to provide SurfaceView. These optional interface should be used during
- * {@link PlaybackGlue#setHost(PlaybackGlueHost)}.
+ * Subclass of PlaybackGlueHost may implement optional interfaces:
+ * <li>{@link SurfaceHolderGlueHost} to provide SurfaceView for video playback.</li>
+ * <li>{@link PlaybackSeekUi} to provide seek UI to glue</li>
+ * These optional interfaces should be accessed by glue in
+ * {@link PlaybackGlue#onAttachedToHost(PlaybackGlueHost)}.
  */
 public abstract class PlaybackGlueHost {
     PlaybackGlue mGlue;
@@ -50,50 +53,128 @@
      */
     public abstract static class HostCallback {
         /**
-         * Callback triggered once the host(fragment) has started.
+         * Client triggered once the host(fragment) has started.
          */
         public void onHostStart() {
         }
 
         /**
-         * Callback triggered once the host(fragment) has stopped.
+         * Client triggered once the host(fragment) has stopped.
          */
         public void onHostStop() {
         }
 
         /**
-         * Callback triggered once the host(fragment) has paused.
+         * Client triggered once the host(fragment) has paused.
          */
         public void onHostPause() {
         }
 
         /**
-         * Callback triggered once the host(fragment) has resumed.
+         * Client triggered once the host(fragment) has resumed.
          */
         public void onHostResume() {
         }
 
         /**
-         * Callback triggered once the host(fragment) has been destroyed.
+         * Client triggered once the host(fragment) has been destroyed.
          */
         public void onHostDestroy() {
         }
     }
 
     /**
+     * Optional Client that implemented by PlaybackGlueHost to respond to player event.
+     */
+    public static class PlayerCallback {
+        /**
+         * Size of the video changes, the Host should adjust SurfaceView's layout width and height.
+         * @param videoWidth
+         * @param videoHeight
+         */
+        public void onVideoSizeChanged(int videoWidth, int videoHeight) {
+        }
+
+        /**
+         * notify media starts/stops buffering/preparing. The Host could start or stop
+         * progress bar.
+         * @param start True for buffering start, false otherwise.
+         */
+        public void onBufferingStateChanged(boolean start) {
+        }
+
+        /**
+         * notify media has error. The Host could show error dialog.
+         * @param errorCode Optional error code for specific implementation.
+         * @param errorMessage Optional error message for specific implementation.
+         */
+        public void onError(int errorCode, CharSequence errorMessage) {
+        }
+    }
+
+    /**
      * Enables or disables view fading.  If enabled, the view will be faded in when the
      * fragment starts and will fade out after a time period.
+     * @deprecated Use {@link #setControlsOverlayAutoHideEnabled(boolean)}
      */
+    @Deprecated
     public void setFadingEnabled(boolean enable) {
     }
 
     /**
-     * Fade out views immediately.
+     * Enables or disables controls overlay auto hidden.  If enabled, the view will be faded out
+     * after a time period.
+     * @param enabled True to enable auto hidden of controls overlay.
+     *
      */
+    public void setControlsOverlayAutoHideEnabled(boolean enabled) {
+        setFadingEnabled(enabled);
+    }
+
+    /**
+     * Returns true if auto hides controls overlay.
+     * @return True if auto hiding controls overlay.
+     */
+    public boolean isControlsOverlayAutoHideEnabled() {
+        return false;
+    }
+
+    /**
+     * Fades out the playback overlay immediately.
+     * @deprecated Call {@link #hideControlsOverlay(boolean)}
+     */
+    @Deprecated
     public void fadeOut() {
     }
 
     /**
+     * Returns true if controls overlay is visible, false otherwise.
+     *
+     * @return True if controls overlay is visible, false otherwise.
+     * @see #showControlsOverlay(boolean)
+     * @see #hideControlsOverlay(boolean)
+     */
+    public boolean isControlsOverlayVisible() {
+        return true;
+    }
+
+    /**
+     * Hide controls overlay.
+     *
+     * @param runAnimation True to run animation, false otherwise.
+     */
+    public void hideControlsOverlay(boolean runAnimation) {
+    }
+
+    /**
+     * Show controls overlay.
+     *
+     * @param runAnimation True to run animation, false otherwise.
+     */
+    public void showControlsOverlay(boolean runAnimation) {
+    }
+
+    /**
      * Sets the {@link android.view.View.OnKeyListener} on the host. This would trigger
      * the listener when a {@link android.view.KeyEvent} is unhandled by the host.
      */
@@ -139,4 +220,13 @@
         }
     }
 
+    /**
+     * Implemented by PlaybackGlueHost for responding to player events. Such as showing a spinning
+     * wheel progress bar when {@link PlayerCallback#onBufferingStateChanged(boolean)}.
+     * @return PlayerEventCallback that Host supports, null if not supported.
+     */
+    public PlayerCallback getPlayerCallback() {
+        return null;
+    }
+
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlaybackTransportControlGlue.java b/v17/leanback/src/android/support/v17/leanback/media/PlaybackTransportControlGlue.java
new file mode 100644
index 0000000..e136bcc
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/media/PlaybackTransportControlGlue.java
@@ -0,0 +1,819 @@
+/*
+ * 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.media;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Message;
+import android.support.annotation.CallSuper;
+import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter;
+import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.ControlButtonPresenterSelector;
+import android.support.v17.leanback.widget.ObjectAdapter;
+import android.support.v17.leanback.widget.OnActionClickedListener;
+import android.support.v17.leanback.widget.PlaybackControlsRow;
+import android.support.v17.leanback.widget.PlaybackRowPresenter;
+import android.support.v17.leanback.widget.PlaybackSeekDataProvider;
+import android.support.v17.leanback.widget.PlaybackSeekUi;
+import android.support.v17.leanback.widget.PlaybackTransportRowPresenter;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+
+import java.lang.ref.WeakReference;
+import java.util.List;
+
+/**
+ * A helper class for managing a {@link PlaybackControlsRow} being displayed in
+ * {@link PlaybackGlueHost}, it supports standard playback control actions play/pause, and
+ * skip next/previous. This helper class is a glue layer in that manages interaction between the
+ * leanback UI components {@link PlaybackControlsRow} {@link PlaybackTransportRowPresenter}
+ * and a functional {@link PlayerAdapter} which represents the underlying
+ * media player.
+ *
+ * <p>App must pass a {@link PlayerAdapter} in constructor for a specific
+ * implementation e.g. a {@link MediaPlayerAdapter}.
+ * </p>
+ *
+ * <p>The glue has two actions bar: primary actions bar and secondary actions bar. App
+ * can provide additional actions by overriding {@link #onCreatePrimaryActions} and / or
+ * {@link #onCreateSecondaryActions} and respond to actions by override
+ * {@link #onActionClicked(Action)}.
+ * </p>
+ *
+ * <p> It's also subclass's responsibility to implement the "repeat mode" in
+ * {@link #onPlayCompleted()}.
+ * </p>
+ *
+ * <p>
+ * Apps calls {@link #setSeekProvider(PlaybackSeekDataProvider)} to provide seek data. If the
+ * {@link PlaybackGlueHost} is instance of {@link PlaybackSeekUi}, the provider will be passed to
+ * PlaybackGlueHost to render thumb bitmaps.
+ * </p>
+ * Sample Code:
+ * <pre><code>
+ * public class MyVideoFragment extends VideoFragment {
+ *     &#64;Override
+ *     public void onCreate(Bundle savedInstanceState) {
+ *         super.onCreate(savedInstanceState);
+ *         final PlaybackTransportControlGlue<MediaPlayerAdapter> playerGlue =
+ *                 new PlaybackTransportControlGlue(getActivity(),
+ *                         new MediaPlayerAdapter(getActivity()));
+ *         playerGlue.setHost(new VideoFragmentGlueHost(this));
+ *         playerGlue.addPlayerCallback(new PlaybackGlue.PlayerCallback() {
+ *             &#64;Override
+ *             public void onPreparedStateChanged(PlaybackGlue glue) {
+ *                 if (glue.isPrepared()) {
+ *                     playerGlue.setSeekProvider(new MySeekProvider());
+ *                     playerGlue.play();
+ *                 }
+ *             }
+ *         });
+ *         playerGlue.setSubtitle("Leanback artist");
+ *         playerGlue.setTitle("Leanback team at work");
+ *         String uriPath = "android.resource://com.example.android.leanback/raw/video";
+ *         playerGlue.getPlayerAdapter().setDataSource(Uri.parse(uriPath));
+ *     }
+ * }
+ * </code></pre>
+ * @param <T> Type of {@link PlayerAdapter} passed in constructor.
+ */
+public class PlaybackTransportControlGlue<T extends PlayerAdapter> extends PlaybackGlue
+        implements OnActionClickedListener, View.OnKeyListener {
+
+    static final String TAG = "PlaybackTransportGlue";
+    static final boolean DEBUG = false;
+
+    static final int MSG_UPDATE_PLAYBACK_STATE = 100;
+    private static final int UPDATE_PLAYBACK_STATE_DELAY_MS = 2000;
+
+    final T mPlayerAdapter;
+    PlaybackControlsRow mControlsRow;
+    PlaybackRowPresenter mControlsRowPresenter;
+    PlaybackControlsRow.PlayPauseAction mPlayPauseAction;
+    boolean mIsPlaying = true;
+    boolean mFadeWhenPlaying = true;
+
+    CharSequence mSubtitle;
+    CharSequence mTitle;
+    Drawable mCover;
+
+    PlaybackSeekDataProvider mSeekProvider;
+    boolean mSeekEnabled;
+    private PlaybackGlueHost.PlayerCallback mPlayerCallback;
+
+    static class UpdatePlaybackStateHandler extends Handler {
+        @Override
+        public void handleMessage(Message msg) {
+            if (msg.what == MSG_UPDATE_PLAYBACK_STATE) {
+                PlaybackTransportControlGlue glue =
+                        ((WeakReference<PlaybackTransportControlGlue>) msg.obj).get();
+                if (glue != null) {
+                    glue.updatePlaybackState();
+                }
+            }
+        }
+    }
+
+    static final Handler sHandler = new UpdatePlaybackStateHandler();
+
+    final WeakReference<PlaybackTransportControlGlue> mGlueWeakReference =  new WeakReference(this);
+
+    final PlayerAdapter.Callback mAdapterCallback = new PlayerAdapter
+            .Callback() {
+
+        @Override
+        public void onPlayStateChanged(PlayerAdapter wrapper) {
+            if (DEBUG) Log.v(TAG, "onPlayStateChanged");
+            PlaybackTransportControlGlue.this.onPlayStateChanged();
+        }
+
+        @Override
+        public void onCurrentPositionChanged(PlayerAdapter wrapper) {
+            if (DEBUG) Log.v(TAG, "onCurrentPositionChanged");
+            PlaybackTransportControlGlue.this.onUpdateProgress();
+        }
+
+        @Override
+        public void onBufferedPositionChanged(PlayerAdapter wrapper) {
+            if (DEBUG) Log.v(TAG, "onBufferedPositionChanged");
+            PlaybackTransportControlGlue.this.onUpdateBufferedProgress();
+        }
+
+        @Override
+        public void onDurationChanged(PlayerAdapter wrapper) {
+            if (DEBUG) Log.v(TAG, "onDurationChanged");
+            PlaybackTransportControlGlue.this.onUpdateDuration();
+        }
+
+        @Override
+        public void onPlayCompleted(PlayerAdapter wrapper) {
+            if (DEBUG) Log.v(TAG, "onPlayCompleted");
+            PlaybackTransportControlGlue.this.onPlayCompleted();
+        }
+
+        @Override
+        public void onPreparedStateChanged(PlayerAdapter wrapper) {
+            if (DEBUG) Log.v(TAG, "onPreparedStateChanged");
+            PlaybackTransportControlGlue.this.onPreparedStateChanged();
+        }
+
+        @Override
+        public void onVideoSizeChanged(PlayerAdapter wrapper, int width, int height) {
+            if (mPlayerCallback != null) {
+                mPlayerCallback.onVideoSizeChanged(width, height);
+            }
+        }
+
+        @Override
+        public void onError(PlayerAdapter wrapper, int errorCode, String errorMessage) {
+            if (mPlayerCallback != null) {
+                mPlayerCallback.onError(errorCode, errorMessage);
+            }
+        }
+
+        @Override
+        public void onBufferingStateChanged(PlayerAdapter wrapper, boolean start) {
+            if (mPlayerCallback != null) {
+                mPlayerCallback.onBufferingStateChanged(start);
+            }
+        }
+    };
+
+    /**
+     * Constructor for the glue.
+     *
+     * @param context
+     * @param impl Implementation to underlying media player.
+     */
+    public PlaybackTransportControlGlue(Context context, T impl) {
+        super(context);
+        mPlayerAdapter = impl;
+        mPlayerAdapter.setCallback(mAdapterCallback);
+    }
+
+    public final T getPlayerAdapter() {
+        return mPlayerAdapter;
+    }
+
+    @Override
+    protected void onAttachedToHost(PlaybackGlueHost host) {
+        super.onAttachedToHost(host);
+        host.setOnKeyInterceptListener(this);
+        host.setOnActionClickedListener(this);
+        onCreateDefaultControlsRow();
+        onCreateDefaultRowPresenter();
+        host.setPlaybackRowPresenter(getPlaybackRowPresenter());
+        host.setPlaybackRow(getControlsRow());
+        if (host instanceof PlaybackSeekUi) {
+            ((PlaybackSeekUi) host).setPlaybackSeekUiClient(mPlaybackSeekUiClient);
+        }
+        mPlayerCallback = host.getPlayerCallback();
+        mPlayerAdapter.onAttachedToHost(host);
+    }
+
+    @Override
+    protected void onHostStart() {
+        mPlayerAdapter.setProgressUpdatingEnabled(true);
+    }
+
+    @Override
+    protected void onHostStop() {
+        mPlayerAdapter.setProgressUpdatingEnabled(false);
+    }
+
+    @Override
+    protected void onDetachedFromHost() {
+        if (getHost() instanceof PlaybackSeekUi) {
+            ((PlaybackSeekUi) getHost()).setPlaybackSeekUiClient(null);
+        }
+        if (mPlayerCallback != null) {
+            mPlayerCallback.onBufferingStateChanged(false);
+        }
+        mPlayerCallback = null;
+        mPlayerAdapter.onDetachedFromHost();
+        mPlayerAdapter.setProgressUpdatingEnabled(false);
+        super.onDetachedFromHost();
+    }
+
+    void onCreateDefaultControlsRow() {
+        if (mControlsRow == null) {
+            PlaybackControlsRow controlsRow = new PlaybackControlsRow(this);
+            setControlsRow(controlsRow);
+        }
+    }
+
+    void onCreateDefaultRowPresenter() {
+        if (mControlsRowPresenter == null) {
+            final AbstractDetailsDescriptionPresenter detailsPresenter =
+                    new AbstractDetailsDescriptionPresenter() {
+                        @Override
+                        protected void onBindDescription(ViewHolder
+                                viewHolder, Object obj) {
+                            PlaybackTransportControlGlue glue = (PlaybackTransportControlGlue) obj;
+                            viewHolder.getTitle().setText(glue.getTitle());
+                            viewHolder.getSubtitle().setText(glue.getSubtitle());
+                        }
+                    };
+
+            PlaybackTransportRowPresenter rowPresenter = new PlaybackTransportRowPresenter() {
+                @Override
+                protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) {
+                    super.onBindRowViewHolder(vh, item);
+                    vh.setOnKeyListener(PlaybackTransportControlGlue.this);
+                }
+                @Override
+                protected void onUnbindRowViewHolder(RowPresenter.ViewHolder vh) {
+                    super.onUnbindRowViewHolder(vh);
+                    vh.setOnKeyListener(null);
+                }
+            };
+            rowPresenter.setDescriptionPresenter(detailsPresenter);
+            setPlaybackRowPresenter(rowPresenter);
+        }
+    }
+
+    /**
+     * Sets the controls to fade after a timeout when media is playing.
+     */
+    public void setFadingEnabled(boolean enable) {
+        mFadeWhenPlaying = enable;
+        if (!mFadeWhenPlaying && getHost() != null) {
+            getHost().setControlsOverlayAutoHideEnabled(false);
+        }
+    }
+
+    /**
+     * Returns true if controls are set to fade when media is playing.
+     */
+    public boolean isFadingEnabled() {
+        return mFadeWhenPlaying;
+    }
+
+    /**
+     * Sets the controls row to be managed by the glue layer. If
+     * {@link PlaybackControlsRow#getPrimaryActionsAdapter()} is not provided, a default
+     * {@link ArrayObjectAdapter} will be created and initialized in
+     * {@link #onCreatePrimaryActions(ArrayObjectAdapter)}. If
+     * {@link PlaybackControlsRow#getSecondaryActionsAdapter()} is not provided, a default
+     * {@link ArrayObjectAdapter} will be created and initialized in
+     * {@link #onCreateSecondaryActions(ArrayObjectAdapter)}.
+     * The primary actions and playback state related aspects of the row
+     * are updated by the glue.
+     */
+    public void setControlsRow(PlaybackControlsRow controlsRow) {
+        mControlsRow = controlsRow;
+        mControlsRow.setCurrentPosition(-1);
+        mControlsRow.setDuration(-1);
+        mControlsRow.setBufferedPosition(-1);
+        if (mControlsRow.getPrimaryActionsAdapter() == null) {
+            ArrayObjectAdapter adapter = new ArrayObjectAdapter(
+                    new ControlButtonPresenterSelector());
+            onCreatePrimaryActions(adapter);
+            mControlsRow.setPrimaryActionsAdapter(adapter);
+        }
+        // Add secondary actions
+        if (mControlsRow.getSecondaryActionsAdapter() == null) {
+            ArrayObjectAdapter secondaryActions = new ArrayObjectAdapter(
+                    new ControlButtonPresenterSelector());
+            onCreateSecondaryActions(secondaryActions);
+            getControlsRow().setSecondaryActionsAdapter(secondaryActions);
+        }
+        updateControlsRow();
+    }
+
+    /**
+     * Sets the controls row Presenter to be managed by the glue layer.
+     */
+    public void setPlaybackRowPresenter(PlaybackRowPresenter presenter) {
+        mControlsRowPresenter = presenter;
+    }
+
+    /**
+     * Returns the playback controls row managed by the glue layer.
+     */
+    public PlaybackControlsRow getControlsRow() {
+        return mControlsRow;
+    }
+
+    /**
+     * Returns the playback controls row Presenter managed by the glue layer.
+     */
+    public PlaybackRowPresenter getPlaybackRowPresenter() {
+        return mControlsRowPresenter;
+    }
+
+    /**
+     * Handles action clicks.  A subclass may override this add support for additional actions.
+     */
+    @Override
+    public void onActionClicked(Action action) {
+        dispatchAction(action, null);
+    }
+
+    /**
+     * Handles key events and returns true if handled.  A subclass may override this to provide
+     * additional support.
+     */
+    @Override
+    public boolean onKey(View v, int keyCode, KeyEvent event) {
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_DPAD_UP:
+            case KeyEvent.KEYCODE_DPAD_DOWN:
+            case KeyEvent.KEYCODE_DPAD_RIGHT:
+            case KeyEvent.KEYCODE_DPAD_LEFT:
+            case KeyEvent.KEYCODE_BACK:
+            case KeyEvent.KEYCODE_ESCAPE:
+                return false;
+        }
+        final ObjectAdapter primaryActionsAdapter = mControlsRow.getPrimaryActionsAdapter();
+        Action action = mControlsRow.getActionForKeyCode(primaryActionsAdapter, keyCode);
+        if (action == null) {
+            action = mControlsRow.getActionForKeyCode(mControlsRow.getSecondaryActionsAdapter(),
+                    keyCode);
+        }
+
+        if (action != null) {
+            if (event.getAction() == KeyEvent.ACTION_DOWN) {
+                dispatchAction(action, event);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Called when the given action is invoked, either by click or keyevent.
+     */
+    boolean dispatchAction(Action action, KeyEvent keyEvent) {
+        boolean handled = false;
+        if (action instanceof PlaybackControlsRow.PlayPauseAction) {
+            boolean canPlay = keyEvent == null
+                    || keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
+                    || keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY;
+            boolean canPause = keyEvent == null
+                    || keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
+                    || keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PAUSE;
+            //            PLAY_PAUSE    PLAY      PAUSE
+            // playing    paused                  paused
+            // paused     playing       playing
+            // ff/rw      playing       playing   paused
+            if (canPause
+                    && (canPlay ? mIsPlaying :
+                        !mIsPlaying)) {
+                mIsPlaying = false;
+                pause();
+            } else if (canPlay && !mIsPlaying) {
+                mIsPlaying = true;
+                play();
+            }
+            updatePlaybackStatusAfterUserAction();
+            handled = true;
+        } else if (action instanceof PlaybackControlsRow.SkipNextAction) {
+            next();
+            handled = true;
+        } else if (action instanceof PlaybackControlsRow.SkipPreviousAction) {
+            previous();
+            handled = true;
+        }
+        return handled;
+    }
+
+    private void updateControlsRow() {
+        onMetadataChanged();
+        sHandler.removeMessages(MSG_UPDATE_PLAYBACK_STATE, mGlueWeakReference);
+        updatePlaybackState();
+    }
+
+    private void updatePlaybackStatusAfterUserAction() {
+        updatePlaybackState(mIsPlaying);
+        // Sync playback state after a delay
+        sHandler.removeMessages(MSG_UPDATE_PLAYBACK_STATE, mGlueWeakReference);
+        sHandler.sendMessageDelayed(sHandler.obtainMessage(MSG_UPDATE_PLAYBACK_STATE,
+                mGlueWeakReference), UPDATE_PLAYBACK_STATE_DELAY_MS);
+    }
+
+    @Override
+    public final boolean isPlaying() {
+        return mPlayerAdapter.isPlaying();
+    }
+
+    @Override
+    public final void play() {
+        mPlayerAdapter.play();
+    }
+
+    @Override
+    public void pause() {
+        mPlayerAdapter.pause();
+    }
+
+    private void updatePlaybackState(boolean isPlaying) {
+        if (mControlsRow == null) {
+            return;
+        }
+
+        if (!isPlaying) {
+            onUpdateProgress();
+            mPlayerAdapter.setProgressUpdatingEnabled(mPlaybackSeekUiClient.mIsSeek);
+        } else {
+            mPlayerAdapter.setProgressUpdatingEnabled(true);
+        }
+
+        if (mFadeWhenPlaying && getHost() != null) {
+            getHost().setControlsOverlayAutoHideEnabled(isPlaying);
+        }
+
+        if (mPlayPauseAction != null) {
+            int index = !isPlaying
+                    ? PlaybackControlsRow.PlayPauseAction.PLAY
+                    : PlaybackControlsRow.PlayPauseAction.PAUSE;
+            if (mPlayPauseAction.getIndex() != index) {
+                mPlayPauseAction.setIndex(index);
+                notifyItemChanged((ArrayObjectAdapter) getControlsRow().getPrimaryActionsAdapter(),
+                        mPlayPauseAction);
+            }
+        }
+    }
+
+    private static void notifyItemChanged(ArrayObjectAdapter adapter, Object object) {
+        int index = adapter.indexOf(object);
+        if (index >= 0) {
+            adapter.notifyArrayItemRangeChanged(index, 1);
+        }
+    }
+
+    /**
+     * May be overridden to add primary actions to the adapter. Default implementation add
+     * {@link PlaybackControlsRow.PlayPauseAction}.
+     *
+     * @param primaryActionsAdapter The adapter to add primary {@link Action}s.
+     */
+    protected void onCreatePrimaryActions(ArrayObjectAdapter primaryActionsAdapter) {
+        primaryActionsAdapter.add(mPlayPauseAction =
+                new PlaybackControlsRow.PlayPauseAction(getContext()));
+    }
+
+    /**
+     * May be overridden to add secondary actions to the adapter.
+     *
+     * @param secondaryActionsAdapter The adapter you need to add the {@link Action}s to.
+     */
+    protected void onCreateSecondaryActions(ArrayObjectAdapter secondaryActionsAdapter) {
+    }
+
+    void onUpdateProgress() {
+        if (mControlsRow != null && !mPlaybackSeekUiClient.mIsSeek) {
+            mControlsRow.setCurrentPosition(mPlayerAdapter.isPrepared()
+                    ? mPlayerAdapter.getCurrentPosition() : -1);
+        }
+    }
+
+    void onUpdateBufferedProgress() {
+        if (mControlsRow != null) {
+            mControlsRow.setBufferedPosition(mPlayerAdapter.getBufferedPosition());
+        }
+    }
+
+    void onUpdateDuration() {
+        if (mControlsRow != null) {
+            mControlsRow.setDuration(
+                    mPlayerAdapter.isPrepared() ? mPlayerAdapter.getDuration() : -1);
+        }
+    }
+
+    /**
+     * @return The duration of the media item in milliseconds.
+     */
+    public final long getDuration() {
+        return mPlayerAdapter.getDuration();
+    }
+
+    /**
+     * @return The current position of the media item in milliseconds.
+     */
+    public final long getCurrentPosition() {
+        return mPlayerAdapter.getCurrentPosition();
+    }
+
+    /**
+     * @return The current buffered position of the media item in milliseconds.
+     */
+    public final long getBufferedPosition() {
+        return mPlayerAdapter.getBufferedPosition();
+    }
+
+    @Override
+    public final boolean isPrepared() {
+        return mPlayerAdapter.isPrepared();
+    }
+
+    /**
+     * Event when ready state for play changes.
+     */
+    @CallSuper
+    protected void onPreparedStateChanged() {
+        onUpdateDuration();
+        List<PlayerCallback> callbacks = getPlayerCallbacks();
+        if (callbacks != null) {
+            for (int i = 0, size = callbacks.size(); i < size; i++) {
+                callbacks.get(i).onPreparedStateChanged(this);
+            }
+        }
+    }
+
+    /**
+     * Sets the drawable representing cover image. The drawable will be rendered by default
+     * description presenter in
+     * {@link PlaybackTransportRowPresenter#setDescriptionPresenter(Presenter)}.
+     * @param cover The drawable representing cover image.
+     */
+    public void setArt(Drawable cover) {
+        if (mCover == cover) {
+            return;
+        }
+        this.mCover = cover;
+        mControlsRow.setImageDrawable(mCover);
+        if (getHost() != null) {
+            getHost().notifyPlaybackRowChanged();
+        }
+    }
+
+    /**
+     * @return The drawable representing cover image.
+     */
+    public Drawable getArt() {
+        return mCover;
+    }
+
+    /**
+     * Sets the media subtitle. The subtitle will be rendered by default description presenter
+     * {@link PlaybackTransportRowPresenter#setDescriptionPresenter(Presenter)}.
+     * @param subtitle Subtitle to set.
+     */
+    public void setSubtitle(CharSequence subtitle) {
+        if (subtitle == null ? mSubtitle == null : subtitle.equals(mSubtitle)) {
+            return;
+        }
+        mSubtitle = subtitle;
+        if (getHost() != null) {
+            getHost().notifyPlaybackRowChanged();
+        }
+    }
+
+    /**
+     * Return The media subtitle.
+     */
+    public CharSequence getSubtitle() {
+        return mSubtitle;
+    }
+
+    /**
+     * Sets the media title. The title will be rendered by default description presenter
+     * {@link PlaybackTransportRowPresenter#setDescriptionPresenter(Presenter)}.
+     */
+    public void setTitle(CharSequence title) {
+        if (title == null ? mTitle == null : title.equals(mTitle)) {
+            return;
+        }
+        mTitle = title;
+        if (getHost() != null) {
+            getHost().notifyPlaybackRowChanged();
+        }
+    }
+
+    /**
+     * Returns the title of the media item.
+     */
+    public CharSequence getTitle() {
+        return mTitle;
+    }
+
+    /**
+     * Event when metadata changed
+     */
+    void onMetadataChanged() {
+        if (mControlsRow == null) {
+            return;
+        }
+
+        if (DEBUG) Log.v(TAG, "updateRowMetadata");
+
+        mControlsRow.setImageDrawable(getArt());
+        mControlsRow.setDuration(mPlayerAdapter.getDuration());
+        mControlsRow.setCurrentPosition(mPlayerAdapter.getCurrentPosition());
+
+        if (getHost() != null) {
+            getHost().notifyPlaybackRowChanged();
+        }
+    }
+
+    void updatePlaybackState() {
+        mIsPlaying = mPlayerAdapter.isPlaying();
+        updatePlaybackState(mIsPlaying);
+    }
+
+    /**
+     * Event when play state changed.
+     */
+    @CallSuper
+    protected void onPlayStateChanged() {
+        if (sHandler.hasMessages(MSG_UPDATE_PLAYBACK_STATE, mGlueWeakReference)) {
+            sHandler.removeMessages(MSG_UPDATE_PLAYBACK_STATE, mGlueWeakReference);
+            if (mPlayerAdapter.isPlaying() != mIsPlaying) {
+                if (DEBUG) Log.v(TAG, "Status expectation mismatch, delaying update");
+                sHandler.sendMessageDelayed(sHandler.obtainMessage(MSG_UPDATE_PLAYBACK_STATE,
+                        mGlueWeakReference), UPDATE_PLAYBACK_STATE_DELAY_MS);
+            } else {
+                if (DEBUG) Log.v(TAG, "Update state matches expectation");
+                updatePlaybackState();
+            }
+        } else {
+            updatePlaybackState();
+        }
+        List<PlayerCallback> callbacks = getPlayerCallbacks();
+        if (callbacks != null) {
+            for (int i = 0, size = callbacks.size(); i < size; i++) {
+                callbacks.get(i).onPlayStateChanged(this);
+            }
+        }
+    }
+
+    /**
+     * Event when play finishes, subclass may handling repeat mode here.
+     */
+    @CallSuper
+    protected void onPlayCompleted() {
+        List<PlayerCallback> callbacks = getPlayerCallbacks();
+        if (callbacks != null) {
+            for (int i = 0, size = callbacks.size(); i < size; i++) {
+                callbacks.get(i).onPlayCompleted(this);
+            }
+        }
+    }
+
+    final SeekUiClient mPlaybackSeekUiClient = new SeekUiClient();
+
+    class SeekUiClient extends PlaybackSeekUi.Client {
+        boolean mPausedBeforeSeek;
+        long mPositionBeforeSeek;
+        long mLastUserPosition;
+        boolean mIsSeek;
+
+        @Override
+        public PlaybackSeekDataProvider getPlaybackSeekDataProvider() {
+            return mSeekProvider;
+        }
+
+        @Override
+        public boolean isSeekEnabled() {
+            return mSeekProvider != null || mSeekEnabled;
+        }
+
+        @Override
+        public void onSeekStarted() {
+            mIsSeek = true;
+            mPausedBeforeSeek = !isPlaying();
+            mPlayerAdapter.setProgressUpdatingEnabled(true);
+            // if we seek thumbnails, we don't need save original position because current
+            // position is not changed during seeking.
+            // otherwise we will call seekTo() and may need to restore the original position.
+            mPositionBeforeSeek = mSeekProvider == null ? mPlayerAdapter.getCurrentPosition() : -1;
+            mLastUserPosition = -1;
+            pause();
+        }
+
+        @Override
+        public void onSeekPositionChanged(long pos) {
+            if (mSeekProvider == null) {
+                mPlayerAdapter.seekTo(pos);
+            } else {
+                mLastUserPosition = pos;
+            }
+            if (mControlsRow != null) {
+                mControlsRow.setCurrentPosition(pos);
+            }
+        }
+
+        @Override
+        public void onSeekFinished(boolean cancelled) {
+            if (!cancelled) {
+                if (mLastUserPosition > 0) {
+                    seekTo(mLastUserPosition);
+                }
+            } else {
+                if (mPositionBeforeSeek >= 0) {
+                    seekTo(mPositionBeforeSeek);
+                }
+            }
+            mIsSeek = false;
+            if (!mPausedBeforeSeek) {
+                play();
+            } else {
+                mPlayerAdapter.setProgressUpdatingEnabled(false);
+                // we neeed update UI since PlaybackControlRow still saves previous position.
+                onUpdateProgress();
+            }
+        }
+    };
+
+    /**
+     * Set seek data provider used during user seeking.
+     * @param seekProvider Seek data provider used during user seeking.
+     */
+    public final void setSeekProvider(PlaybackSeekDataProvider seekProvider) {
+        mSeekProvider = seekProvider;
+    }
+
+    /**
+     * Get seek data provider used during user seeking.
+     * @return Seek data provider used during user seeking.
+     */
+    public final PlaybackSeekDataProvider getSeekProvider() {
+        return mSeekProvider;
+    }
+
+    /**
+     * Enable or disable seek when {@link #getSeekProvider()} is null. When true,
+     * {@link PlayerAdapter#seekTo(long)} will be called during user seeking.
+     *
+     * @param seekEnabled True to enable seek, false otherwise
+     */
+    public final void setSeekEnabled(boolean seekEnabled) {
+        mSeekEnabled = seekEnabled;
+    }
+
+    /**
+     * @return True if seek is enabled without {@link PlaybackSeekDataProvider}, false otherwise.
+     */
+    public final boolean isSeekEnabled() {
+        return mSeekEnabled;
+    }
+
+    /**
+     * Seek media to a new position.
+     * @param position New position.
+     */
+    public final void seekTo(long position) {
+        mPlayerAdapter.seekTo(position);
+    }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlayerAdapter.java b/v17/leanback/src/android/support/v17/leanback/media/PlayerAdapter.java
new file mode 100644
index 0000000..ea09d04
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/media/PlayerAdapter.java
@@ -0,0 +1,193 @@
+/*
+ * 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;
+
+/**
+ * Base class that wraps underlying media player. The class is used by PlaybackGlue, for example
+ * {@link PlaybackTransportControlGlue} is bound to a PlayerAdapter.
+ * This class is intended to be subclassed, {@link MediaPlayerAdapter} is a concrete subclass
+ * using {@link android.media.MediaPlayer}.
+ */
+public abstract class PlayerAdapter {
+
+    /**
+     * Client for client of PlayerAdapter.
+     */
+    public static class Callback {
+
+        /**
+         * Client for Play/Pause state change. See {@link #isPlaying()}.
+         */
+        public void onPlayStateChanged(PlayerAdapter adapter) {
+        }
+
+        /**
+         * Client for {@link #isPrepared()} changed.
+         * @param adapter The adapter that has changed ready state.
+         */
+        public void onPreparedStateChanged(PlayerAdapter adapter) {
+        }
+
+        /**
+         * Client when the current media is finished.
+         * @param adapter The adapter that has just finished current media.
+         */
+        public void onPlayCompleted(PlayerAdapter adapter) {
+        }
+
+        /**
+         * Event for {@link #getCurrentPosition()} changed.
+         * @param adapter The adapter whose {@link #getCurrentPosition()} changed.
+         */
+        public void onCurrentPositionChanged(PlayerAdapter adapter) {
+        }
+
+        /**
+         * Event for {@link #getBufferedPosition()} changed.
+         * @param adapter The adapter whose {@link #getBufferedPosition()} changed.
+         */
+        public void onBufferedPositionChanged(PlayerAdapter adapter) {
+        }
+
+        /**
+         * Event for {@link #getDuration()} changed. Usually the duration does not change
+         * after playing except for live stream.
+         * @param adapter The adapter whose {@link #getDuration()} changed.
+         */
+        public void onDurationChanged(PlayerAdapter adapter) {
+        }
+
+        /**
+         * Event for video size changed.
+         * @param adapter The adapter whose video size has been detected or changed.
+         * @param width Intrinsic width of the video.
+         * @param height Intrinsic height of the video.
+         */
+        public void onVideoSizeChanged(PlayerAdapter adapter, int width, int height) {
+        }
+
+        /**
+         * Event for error.
+         * @param adapter The adapter that encounters error.
+         * @param errorCode Optional error code, specific to implementation.
+         * @param errorMessage Optional error message, specific to implementation.
+         */
+        public void onError(PlayerAdapter adapter, int errorCode, String errorMessage) {
+        }
+
+        /**
+         * Event for buffering start or stop. Initial default value is false.
+         * @param adapter The adapter that begins buffering or finishes buffering.
+         * @param start True for buffering start, false otherwise.
+         */
+        public void onBufferingStateChanged(PlayerAdapter adapter, boolean start) {
+        }
+    }
+
+    Callback mCallback;
+
+    /**
+     * Sets callback for event of PlayerAdapter.
+     * @param callback Client for event of PlayerAdapter.
+     */
+    public final void setCallback(Callback callback) {
+        mCallback = callback;
+    }
+
+    /**
+     * Gets callback for event of PlayerAdapter.
+     * @return Client for event of PlayerAdapter.
+     */
+    public final Callback getCallback() {
+        return mCallback;
+    }
+
+    /**
+     * @return True if media is ready for playback, false otherwise.
+     */
+    public boolean isPrepared() {
+        return true;
+    }
+
+    /**
+     * Starts the media player.
+     */
+    public abstract void play();
+
+    /**
+     * Pauses the media player.
+     */
+    public abstract void pause();
+
+    /**
+     * Seek to new position.
+     * @param positionInMs New position in milliseconds.
+     */
+    public void seekTo(long positionInMs) {
+    }
+
+    /**
+     * Implement this method to enable or disable progress updating.
+     * @param enable True to enable progress updating, false otherwise.
+     */
+    public void setProgressUpdatingEnabled(boolean enable) {
+    }
+
+    /**
+     * Returns true if media is currently playing.
+     */
+    public boolean isPlaying() {
+        return false;
+    }
+
+    /**
+     * Returns the duration of the media item in milliseconds.
+     */
+    public long getDuration() {
+        return 0;
+    }
+
+    /**
+     * Returns the current position of the media item in milliseconds.
+     */
+    public long getCurrentPosition() {
+        return 0;
+    }
+
+    /**
+     * Returns the current buffered position of the media item in milliseconds.
+     */
+    public long getBufferedPosition() {
+        return 0;
+    }
+
+    /**
+     * This method is called attached to associated {@link PlaybackGlueHost}.
+     * @param host
+     */
+    public void onAttachedToHost(PlaybackGlueHost host) {
+    }
+
+    /**
+     * This method is called when current associated {@link PlaybackGlueHost} is attached to a
+     * different {@link PlaybackGlue} or {@link PlaybackGlueHost} is destroyed. Subclass may
+     * override. A typical implementation will release resources (e.g. MediaPlayer or connection
+     * to playback service) in this method.
+     */
+    public void onDetachedFromHost() {
+    }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/media/SurfaceHolderGlueHost.java b/v17/leanback/src/android/support/v17/leanback/media/SurfaceHolderGlueHost.java
index 119626a..679a19a 100644
--- a/v17/leanback/src/android/support/v17/leanback/media/SurfaceHolderGlueHost.java
+++ b/v17/leanback/src/android/support/v17/leanback/media/SurfaceHolderGlueHost.java
@@ -24,7 +24,6 @@
  * the surface holder callback during {@link PlaybackGlue#setHost(PlaybackGlueHost)}.
  *
  * @see PlaybackGlue#setHost(PlaybackGlueHost)
- * @see MediaPlayerGlue
  */
 public interface SurfaceHolderGlueHost {
     /**
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ControlBar.java b/v17/leanback/src/android/support/v17/leanback/widget/ControlBar.java
index be5fd83..1942ae0 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/ControlBar.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ControlBar.java
@@ -31,6 +31,7 @@
     private int mChildMarginFromCenter;
     private OnChildFocusedListener mOnChildFocusedListener;
     int mLastFocusIndex = -1;
+    boolean mDefaultFocusToMiddle = true;
 
     public ControlBar(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -40,11 +41,19 @@
         super(context, attrs, defStyle);
     }
 
+    void setDefaultFocusToMiddle(boolean defaultFocusToMiddle) {
+        mDefaultFocusToMiddle = defaultFocusToMiddle;
+    }
+
+    int getDefaultFocusIndex() {
+        return mDefaultFocusToMiddle ? getChildCount() / 2 : 0;
+    }
+
     @Override
     protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
         if (getChildCount() > 0) {
             int index = mLastFocusIndex >= 0 && mLastFocusIndex < getChildCount()
-                    ? mLastFocusIndex : getChildCount() / 2;
+                    ? mLastFocusIndex : getDefaultFocusIndex();
             if (getChildAt(index).requestFocus(direction, previouslyFocusedRect)) {
                 return true;
             }
@@ -58,7 +67,7 @@
             if (mLastFocusIndex >= 0 && mLastFocusIndex < getChildCount()) {
                 views.add(getChildAt(mLastFocusIndex));
             } else if (getChildCount() > 0) {
-                views.add(getChildAt(getChildCount() / 2));
+                views.add(getChildAt(getDefaultFocusIndex()));
             }
         } else {
             super.addFocusables(views, direction, focusableMode);
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ControlBarPresenter.java b/v17/leanback/src/android/support/v17/leanback/widget/ControlBarPresenter.java
index 4314fce..a3319ba 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/ControlBarPresenter.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ControlBarPresenter.java
@@ -82,6 +82,7 @@
             if (mControlBar == null) {
                 throw new IllegalStateException("Couldn't find control_bar");
             }
+            mControlBar.setDefaultFocusToMiddle(mDefaultFocusToMiddle);
             mControlBar.setOnChildFocusedListener(new ControlBar.OnChildFocusedListener() {
                 @Override
                 public void onChildFocusedListener(View child, View focused) {
@@ -185,6 +186,7 @@
     private int mLayoutResourceId;
     private static int sChildMarginDefault;
     private static int sControlIconWidth;
+    boolean mDefaultFocusToMiddle = true;
 
     /**
      * Constructor for a ControlBarPresenter.
@@ -281,4 +283,12 @@
         }
         return sControlIconWidth;
     }
+
+    /**
+     * @param defaultFocusToMiddle True for middle item, false for 0.
+     */
+    void setDefaultFocusToMiddle(boolean defaultFocusToMiddle) {
+        mDefaultFocusToMiddle = defaultFocusToMiddle;
+    }
+
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRow.java b/v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRow.java
index f54a454..3ec6f93 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRow.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRow.java
@@ -45,6 +45,35 @@
 public class PlaybackControlsRow extends Row {
 
     /**
+     * Listener for progress or duration change.
+     */
+    public static class OnPlaybackProgressCallback {
+        /**
+         * Called when {@link PlaybackControlsRow#getCurrentPosition()} changed.
+         * @param row The PlaybackControlsRow that current time changed.
+         * @param currentTimeMs Current time in milliseconds.
+         */
+        public void onCurrentPositionChanged(PlaybackControlsRow row, long currentTimeMs) {
+        }
+
+        /**
+         * Called when {@link PlaybackControlsRow#getDuration()} changed.
+         * @param row The PlaybackControlsRow that total time changed.
+         * @param totalTime Total time in milliseconds.
+         */
+        public void onDurationChanged(PlaybackControlsRow row, long totalTime) {
+        }
+
+        /**
+         * Called when {@link PlaybackControlsRow#getBufferedPosition()} changed.
+         * @param row The PlaybackControlsRow that buffered progress changed.
+         * @param bufferedProgressMs Buffered time in milliseconds.
+         */
+        public void onBufferedPositionChanged(PlaybackControlsRow row, long bufferedProgressMs) {
+        }
+    }
+
+    /**
      * Base class for an action comprised of a series of icons.
      */
     public static abstract class MultiAction extends Action {
@@ -624,7 +653,7 @@
     private long mTotalTimeMs;
     private long mCurrentTimeMs;
     private long mBufferedProgressMs;
-    private OnPlaybackStateChangedListener mListener;
+    private OnPlaybackProgressCallback mListener;
 
     /**
      * Constructor for a PlaybackControlsRow that displays some details from
@@ -718,39 +747,72 @@
      * Sets the total time in milliseconds for the playback controls row.
      * <p>If set after the row has been bound to a view, the adapter must be notified that
      * this row has changed.</p>
+     * @deprecated Use {@link #setDuration(long)}
      */
+    @Deprecated
     public void setTotalTime(int ms) {
-        setTotalTimeLong((long) ms);
+        setDuration((long) ms);
     }
 
     /**
      * Sets the total time in milliseconds (long type) for the playback controls row.
      * @param ms Total time in milliseconds of long type.
+     * @deprecated Use {@link #setDuration(long)}
      */
+    @Deprecated
     public void setTotalTimeLong(long ms) {
-        mTotalTimeMs = ms;
+        setDuration(ms);
+    }
+
+    /**
+     * Sets the total time in milliseconds (long type) for the playback controls row.
+     * If this row is bound to a view, the view will automatically
+     * be updated to reflect the new value.
+     * @param ms Total time in milliseconds of long type.
+     */
+    public void setDuration(long ms) {
+        if (mTotalTimeMs != ms) {
+            mTotalTimeMs = ms;
+            if (mListener != null) {
+                mListener.onDurationChanged(this, mTotalTimeMs);
+            }
+        }
     }
 
     /**
      * Returns the total time in milliseconds for the playback controls row.
      * @throws ArithmeticException If total time in milliseconds overflows int.
+     * @deprecated use {@link #getDuration()}
      */
+    @Deprecated
     public int getTotalTime() {
         return MathUtil.safeLongToInt(getTotalTimeLong());
     }
 
     /**
      * Returns the total time in milliseconds of long type for the playback controls row.
+     * @deprecated use {@link #getDuration()}
      */
+    @Deprecated
     public long getTotalTimeLong() {
         return mTotalTimeMs;
     }
 
     /**
+     * Returns duration in milliseconds.
+     * @return Duration in milliseconds.
+     */
+    public long getDuration() {
+        return mTotalTimeMs;
+    }
+
+    /**
      * Sets the current time in milliseconds for the playback controls row.
      * If this row is bound to a view, the view will automatically
      * be updated to reflect the new value.
+     * @deprecated use {@link #setCurrentPosition(long)}
      */
+    @Deprecated
     public void setCurrentTime(int ms) {
         setCurrentTimeLong((long) ms);
     }
@@ -758,61 +820,110 @@
     /**
      * Sets the current time in milliseconds for playback controls row in long type.
      * @param ms Current time in milliseconds of long type.
+     * @deprecated use {@link #setCurrentPosition(long)}
      */
+    @Deprecated
     public void setCurrentTimeLong(long ms) {
+        setCurrentPosition(ms);
+    }
+
+    /**
+     * Sets the current time in milliseconds for the playback controls row.
+     * If this row is bound to a view, the view will automatically
+     * be updated to reflect the new value.
+     * @param ms Current time in milliseconds of long type.
+     */
+    public void setCurrentPosition(long ms) {
         if (mCurrentTimeMs != ms) {
             mCurrentTimeMs = ms;
-            currentTimeChanged();
+            if (mListener != null) {
+                mListener.onCurrentPositionChanged(this, mCurrentTimeMs);
+            }
         }
     }
 
     /**
      * Returns the current time in milliseconds for the playback controls row.
      * @throws ArithmeticException If current time in milliseconds overflows int.
+     * @deprecated Use {@link #getCurrentPosition()}
      */
+    @Deprecated
     public int getCurrentTime() {
         return MathUtil.safeLongToInt(getCurrentTimeLong());
     }
 
     /**
      * Returns the current time in milliseconds of long type for playback controls row.
+     * @deprecated Use {@link #getCurrentPosition()}
      */
+    @Deprecated
     public long getCurrentTimeLong() {
         return mCurrentTimeMs;
     }
 
     /**
+     * Returns the current time in milliseconds of long type for playback controls row.
+     */
+    public long getCurrentPosition() {
+        return mCurrentTimeMs;
+    }
+
+    /**
      * Sets the buffered progress for the playback controls row.
      * If this row is bound to a view, the view will automatically
      * be updated to reflect the new value.
+     * @deprecated Use {@link #setBufferedPosition(long)}
      */
+    @Deprecated
     public void setBufferedProgress(int ms) {
-        setBufferedProgressLong((long) ms);
+        setBufferedPosition((long) ms);
+    }
+
+    /**
+     * Sets the buffered progress for the playback controls row.
+     * @param ms Buffered progress in milliseconds of long type.
+     * @deprecated Use {@link #setBufferedPosition(long)}
+     */
+    @Deprecated
+    public void setBufferedProgressLong(long ms) {
+        setBufferedPosition(ms);
     }
 
     /**
      * Sets the buffered progress for the playback controls row.
      * @param ms Buffered progress in milliseconds of long type.
      */
-    public void setBufferedProgressLong(long ms) {
+    public void setBufferedPosition(long ms) {
         if (mBufferedProgressMs != ms) {
             mBufferedProgressMs = ms;
-            bufferedProgressChanged();
+            if (mListener != null) {
+                mListener.onBufferedPositionChanged(this, mBufferedProgressMs);
+            }
         }
     }
-
     /**
      * Returns the buffered progress for the playback controls row.
      * @throws ArithmeticException If buffered progress in milliseconds overflows int.
+     * @deprecated Use {@link #getBufferedPosition()}
      */
+    @Deprecated
     public int getBufferedProgress() {
-        return MathUtil.safeLongToInt(getBufferedProgressLong());
+        return MathUtil.safeLongToInt(getBufferedPosition());
+    }
+
+    /**
+     * Returns the buffered progress of long type for the playback controls row.
+     * @deprecated Use {@link #getBufferedPosition()}
+     */
+    @Deprecated
+    public long getBufferedProgressLong() {
+        return mBufferedProgressMs;
     }
 
     /**
      * Returns the buffered progress of long type for the playback controls row.
      */
-    public long getBufferedProgressLong() {
+    public long getBufferedPosition() {
         return mBufferedProgressMs;
     }
 
@@ -844,34 +955,10 @@
         return null;
     }
 
-    interface OnPlaybackStateChangedListener {
-        public void onCurrentTimeChanged(long currentTimeMs);
-        public void onBufferedProgressChanged(long bufferedProgressMs);
-    }
-
     /**
      * Sets a listener to be called when the playback state changes.
      */
-    void setOnPlaybackStateChangedListener(OnPlaybackStateChangedListener listener) {
+    public void setOnPlaybackProgressChangedListener(OnPlaybackProgressCallback listener) {
         mListener = listener;
     }
-
-    /**
-     * Returns the playback state listener.
-     */
-    OnPlaybackStateChangedListener getOnPlaybackStateChangedListener() {
-        return mListener;
-    }
-
-    private void currentTimeChanged() {
-        if (mListener != null) {
-            mListener.onCurrentTimeChanged(mCurrentTimeMs);
-        }
-    }
-
-    private void bufferedProgressChanged() {
-        if (mListener != null) {
-            mListener.onBufferedProgressChanged(mBufferedProgressMs);
-        }
-    }
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowPresenter.java b/v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowPresenter.java
index ef49129..82cfa79 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowPresenter.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowPresenter.java
@@ -68,14 +68,20 @@
         BoundData mSecondaryBoundData = new BoundData();
         Presenter.ViewHolder mSelectedViewHolder;
         Object mSelectedItem;
-        final PlaybackControlsRow.OnPlaybackStateChangedListener mListener =
-                new PlaybackControlsRow.OnPlaybackStateChangedListener() {
+        final PlaybackControlsRow.OnPlaybackProgressCallback mListener =
+                new PlaybackControlsRow.OnPlaybackProgressCallback() {
             @Override
-            public void onCurrentTimeChanged(long ms) {
+            public void onCurrentPositionChanged(PlaybackControlsRow row, long ms) {
                 mPlaybackControlsPresenter.setCurrentTimeLong(mControlsVh, ms);
             }
+
             @Override
-            public void onBufferedProgressChanged(long ms) {
+            public void onDurationChanged(PlaybackControlsRow row, long ms) {
+                mPlaybackControlsPresenter.setTotalTimeLong(mControlsVh, ms);
+            }
+
+            @Override
+            public void onBufferedPositionChanged(PlaybackControlsRow row, long ms) {
                 mPlaybackControlsPresenter.setSecondaryProgressLong(mControlsVh, ms);
             }
         };
@@ -405,7 +411,7 @@
         mPlaybackControlsPresenter.setTotalTime(vh.mControlsVh, row.getTotalTime());
         mPlaybackControlsPresenter.setCurrentTime(vh.mControlsVh, row.getCurrentTime());
         mPlaybackControlsPresenter.setSecondaryProgress(vh.mControlsVh, row.getBufferedProgress());
-        row.setOnPlaybackStateChangedListener(vh.mListener);
+        row.setOnPlaybackProgressChangedListener(vh.mListener);
     }
 
     private void updateCardLayout(ViewHolder vh, int height) {
@@ -448,7 +454,7 @@
         }
         mPlaybackControlsPresenter.onUnbindViewHolder(vh.mControlsVh);
         mSecondaryControlsPresenter.onUnbindViewHolder(vh.mSecondaryControlsVh);
-        row.setOnPlaybackStateChangedListener(null);
+        row.setOnPlaybackProgressChangedListener(null);
 
         super.onUnbindRowViewHolder(holder);
     }
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowView.java b/v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowView.java
index c10d202..1d0fce0 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowView.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowView.java
@@ -68,4 +68,9 @@
         }
         return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
     }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackSeekDataProvider.java b/v17/leanback/src/android/support/v17/leanback/widget/PlaybackSeekDataProvider.java
new file mode 100644
index 0000000..95493b3
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/PlaybackSeekDataProvider.java
@@ -0,0 +1,76 @@
+/*
+ * 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.graphics.Bitmap;
+
+/**
+ * Class to be implemented by app to provide seeking data and thumbnails to UI.
+ */
+public class PlaybackSeekDataProvider {
+
+    /**
+     * Client to receive result for {@link PlaybackSeekDataProvider#getThumbnail(int,
+     * ResultCallback)}.
+     */
+    public static class ResultCallback {
+
+        /**
+         * Client of thumbnail bitmap being loaded. PlaybackSeekDataProvider must invoke this method
+         * in UI thread such as in {@link android.os.AsyncTask#onPostExecute(Object)}.
+         *
+         * @param bitmap Result of bitmap.
+         * @param index Index of {@link #getSeekPositions()}.
+         */
+        public void onThumbnailLoaded(Bitmap bitmap, int index) {
+        }
+    }
+
+    /**
+     * Get a list of sorted seek positions. The positions should not change after user starts
+     * seeking.
+     *
+     * @return A list of sorted seek positions.
+     */
+    public long[] getSeekPositions() {
+        return null;
+    }
+
+    /**
+     * Called to get thumbnail bitmap. This method is called on UI thread. When provider finds
+     * cache bitmap, it may invoke {@link ResultCallback#onThumbnailLoaded(Bitmap, int)}
+     * immediately. Provider may start background thread and invoke
+     * {@link ResultCallback#onThumbnailLoaded(Bitmap, int)} later in UI thread. The method might
+     * be called multiple times for the same position, PlaybackSeekDataProvider must guarantee
+     * to replace pending {@link ResultCallback} with the new one. When seeking right,
+     * getThumbnail() will be called with increasing index; when seeking left, getThumbnail() will
+     * be called with decreasing index. The increment of index can be used by subclass to determine
+     * prefetch direction.
+     *
+     * @param index Index of position in {@link #getSeekPositions()}.
+     * @param callback The callback to receive the result on UI thread. It may be called within
+     *                 getThumbnail() if hit cache directly.
+     */
+    public void getThumbnail(int index, ResultCallback callback) {
+    }
+
+    /**
+     * Called when seek stops, Provider should cancel pending requests for the thumbnails.
+     */
+    public void reset() {
+    }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackSeekUi.java b/v17/leanback/src/android/support/v17/leanback/widget/PlaybackSeekUi.java
new file mode 100644
index 0000000..3000498
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/PlaybackSeekUi.java
@@ -0,0 +1,84 @@
+/*
+ * 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;
+
+/**
+ * Interface to be implemented by UI component to support seeking. PlaybackGlueHost may implement
+ * the interface to support seeking UI for the PlaybackGlue. There is only one single method
+ * {@link #setPlaybackSeekUiClient(Client)} in the interface. Client (PlaybackGlue) registers
+ * itself as a Client to receive events emitted by PlaybackSeekUi and provide data to the
+ * PlaybackSeekUi.
+ */
+public interface PlaybackSeekUi {
+
+    /**
+     * Client (e.g. PlaybackGlue) to register on PlaybackSeekUi so that it can interact
+     * with Seeking UI. For example client(PlaybackGlue) will pause media when PlaybackSeekUi emits
+     * {@link #onSeekStarted()} event.
+     */
+    class Client {
+
+        /**
+         * Called by PlaybackSeekUi to query client if seek is allowed.
+         * @return True if allow PlaybackSeekUi to start seek, false otherwise.
+         */
+        public boolean isSeekEnabled() {
+            return false;
+        }
+
+        /**
+         * Event for start seeking. Client will typically pause media and save the current position
+         * in the callback.
+         */
+        public void onSeekStarted() {
+        }
+
+        /**
+         * Called by PlaybackSeekUi asking for PlaybackSeekDataProvider. This method will be called
+         * after {@link #isSeekEnabled()} returns true. If client does not provide a
+         * {@link PlaybackSeekDataProvider}, client may directly seek media in
+         * {@link #onSeekPositionChanged(long)}.
+         * @return PlaybackSeekDataProvider or null if no PlaybackSeekDataProvider is available.
+         */
+        public PlaybackSeekDataProvider getPlaybackSeekDataProvider() {
+            return null;
+        }
+
+        /**
+         * Called when user seeks to a different location. This callback is called multiple times
+         * between {@link #onSeekStarted()} and {@link #onSeekFinished(boolean)}.
+         * @param pos Position that user seeks to.
+         */
+        public void onSeekPositionChanged(long pos) {
+        }
+
+        /**
+         * Called when cancelled or confirmed. When cancelled, client should restore playing from
+         * the position before {@link #onSeekStarted()}. When confirmed, client should seek to
+         * last updated {@link #onSeekPositionChanged(long)}.
+         * @param cancelled True if cancelled false if confirmed.
+         */
+        public void onSeekFinished(boolean cancelled) {
+        }
+    }
+
+    /**
+     * Interface to be implemented by UI widget to support PlaybackSeekUi.
+     */
+    void setPlaybackSeekUiClient(Client client);
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackTransportRowPresenter.java b/v17/leanback/src/android/support/v17/leanback/widget/PlaybackTransportRowPresenter.java
new file mode 100644
index 0000000..4c99a3b
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/PlaybackTransportRowPresenter.java
@@ -0,0 +1,801 @@
+/*
+ * 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.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.os.Build;
+import android.support.annotation.ColorInt;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.widget.ControlBarPresenter.OnControlClickedListener;
+import android.support.v17.leanback.widget.ControlBarPresenter.OnControlSelectedListener;
+import android.util.TypedValue;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import java.util.Arrays;
+
+/**
+ * A PlaybackTransportRowPresenter renders a {@link PlaybackControlsRow} to display a
+ * series of playback control buttons. Typically this row will be the first row in a fragment
+ * such as the {@link android.support.v17.leanback.app.PlaybackSupportFragment}.
+ *
+ * <p>The detailed description is rendered using a {@link Presenter} passed in
+ * {@link #setDescriptionPresenter(Presenter)}.  This can be an instance of
+ * {@link AbstractDetailsDescriptionPresenter}.  The application can access the
+ * detailed description ViewHolder from {@link ViewHolder#getDescriptionViewHolder()}.
+ * </p>
+ */
+public class PlaybackTransportRowPresenter extends PlaybackRowPresenter {
+
+    static class BoundData extends PlaybackControlsPresenter.BoundData {
+        ViewHolder mRowViewHolder;
+    }
+
+    /**
+     * A ViewHolder for the PlaybackControlsRow supporting seek UI.
+     */
+    public class ViewHolder extends PlaybackRowPresenter.ViewHolder implements PlaybackSeekUi {
+        final Presenter.ViewHolder mDescriptionViewHolder;
+        final ImageView mImageView;
+        final ViewGroup mDescriptionDock;
+        final ViewGroup mControlsDock;
+        final ViewGroup mSecondaryControlsDock;
+        final TextView mTotalTime;
+        final TextView mCurrentTime;
+        final SeekBar mProgressBar;
+        final ThumbsBar mThumbsBar;
+        long mTotalTimeInMs = Long.MIN_VALUE;
+        long mCurrentTimeInMs = Long.MIN_VALUE;
+        long mSecondaryProgressInMs;
+        final StringBuilder mTempBuilder = new StringBuilder();
+        ControlBarPresenter.ViewHolder mControlsVh;
+        ControlBarPresenter.ViewHolder mSecondaryControlsVh;
+        BoundData mControlsBoundData = new BoundData();
+        BoundData mSecondaryBoundData = new BoundData();
+        Presenter.ViewHolder mSelectedViewHolder;
+        Object mSelectedItem;
+        PlaybackControlsRow.PlayPauseAction mPlayPauseAction;
+        int mThumbHeroIndex = -1;
+
+        Client mSeekClient;
+        boolean mInSeek;
+        PlaybackSeekDataProvider mSeekDataProvider;
+        long[] mPositions;
+        int mPositionsLength;
+
+        final PlaybackControlsRow.OnPlaybackProgressCallback mListener =
+                new PlaybackControlsRow.OnPlaybackProgressCallback() {
+            @Override
+            public void onCurrentPositionChanged(PlaybackControlsRow row, long ms) {
+                setCurrentPosition(ms);
+            }
+
+            @Override
+            public void onDurationChanged(PlaybackControlsRow row, long ms) {
+                setTotalTime(ms);
+            }
+
+            @Override
+            public void onBufferedPositionChanged(PlaybackControlsRow row, long ms) {
+                setBufferedPosition(ms);
+            }
+        };
+
+        void updateProgressInSeek(boolean forward) {
+            long newPos;
+            long pos = mCurrentTimeInMs;
+            if (mPositionsLength > 0) {
+                int index = Arrays.binarySearch(mPositions, 0, mPositionsLength, pos);
+                int thumbHeroIndex;
+                if (forward) {
+                    if (index >= 0) {
+                        // found it, seek to neighbour key position at higher side
+                        if (index < mPositionsLength - 1) {
+                            newPos = mPositions[index + 1];
+                            thumbHeroIndex = index + 1;
+                        } else {
+                            newPos = mTotalTimeInMs;
+                            thumbHeroIndex = index;
+                        }
+                    } else {
+                        // not found, seek to neighbour key position at higher side.
+                        int insertIndex = -1 - index;
+                        if (insertIndex <= mPositionsLength - 1) {
+                            newPos = mPositions[insertIndex];
+                            thumbHeroIndex = insertIndex;
+                        } else {
+                            newPos = mTotalTimeInMs;
+                            thumbHeroIndex = insertIndex > 0 ? insertIndex - 1 : 0;
+                        }
+                    }
+                } else {
+                    if (index >= 0) {
+                        // found it, seek to neighbour key position at lower side.
+                        if (index > 0) {
+                            newPos = mPositions[index - 1];
+                            thumbHeroIndex = index - 1;
+                        } else {
+                            newPos = 0;
+                            thumbHeroIndex = 0;
+                        }
+                    } else {
+                        // not found, seek to neighbour key position at lower side.
+                        int insertIndex = -1 - index;
+                        if (insertIndex > 0) {
+                            newPos = mPositions[insertIndex - 1];
+                            thumbHeroIndex = insertIndex - 1;
+                        } else {
+                            newPos = 0;
+                            thumbHeroIndex = 0;
+                        }
+                    }
+                }
+                updateThumbsInSeek(thumbHeroIndex, forward);
+            } else {
+                long interval = (long) (mTotalTimeInMs * getDefaultSeekIncrement());
+                newPos = pos + (forward ? interval : -interval);
+                if (newPos > mTotalTimeInMs) {
+                    newPos = mTotalTimeInMs;
+                } else if (newPos < 0) {
+                    newPos = 0;
+                }
+            }
+            double ratio = (double) newPos / mTotalTimeInMs;     // Range: [0, 1]
+            mProgressBar.setProgress((int) (ratio * Integer.MAX_VALUE)); // Could safely cast to int
+            mSeekClient.onSeekPositionChanged(newPos);
+        }
+
+        void updateThumbsInSeek(int thumbHeroIndex, boolean forward) {
+            if (mThumbHeroIndex == thumbHeroIndex) {
+                return;
+            }
+
+            final int totalNum = mThumbsBar.getChildCount();
+            if (totalNum < 0 || (totalNum & 1) == 0) {
+                throw new RuntimeException();
+            }
+            final int heroChildIndex = totalNum / 2;
+            final int start = Math.max(thumbHeroIndex - (totalNum / 2), 0);
+            final int end = Math.min(thumbHeroIndex + (totalNum / 2), mPositionsLength - 1);
+            final int newRequestStart;
+            final int newRequestEnd;
+
+            if (mThumbHeroIndex < 0) {
+                // first time
+                newRequestStart = start;
+                newRequestEnd = end;
+            } else {
+                forward = thumbHeroIndex > mThumbHeroIndex;
+                final int oldStart = Math.max(mThumbHeroIndex - (totalNum / 2), 0);
+                final int oldEnd = Math.min(mThumbHeroIndex + (totalNum / 2),
+                        mPositionsLength - 1);
+                if (forward) {
+                    newRequestStart = Math.max(oldEnd + 1, start);
+                    newRequestEnd = end;
+                    // overlapping area directly assign bitmap from previous result
+                    for (int i = start; i <= newRequestStart - 1; i++) {
+                        mThumbsBar.setThumbBitmap(heroChildIndex + (i - thumbHeroIndex),
+                                mThumbsBar.getThumbBitmap(heroChildIndex + (i - mThumbHeroIndex)));
+                    }
+                } else {
+                    newRequestEnd = Math.min(oldStart - 1, end);
+                    newRequestStart = start;
+                    // overlapping area directly assign bitmap from previous result in backward
+                    for (int i = end; i >= newRequestEnd + 1; i--) {
+                        mThumbsBar.setThumbBitmap(heroChildIndex + (i - thumbHeroIndex),
+                                mThumbsBar.getThumbBitmap(heroChildIndex + (i - mThumbHeroIndex)));
+                    }
+                }
+            }
+            // processing new requests with mThumbHeroIndex updated
+            mThumbHeroIndex = thumbHeroIndex;
+            if (forward) {
+                for (int i = newRequestStart; i <= newRequestEnd; i++) {
+                    mSeekDataProvider.getThumbnail(i, mThumbResult);
+                }
+            } else {
+                for (int i = newRequestEnd; i >= newRequestStart; i--) {
+                    mSeekDataProvider.getThumbnail(i, mThumbResult);
+                }
+            }
+            // set thumb bitmaps outside (start , end) to null
+            for (int childIndex = 0; childIndex < heroChildIndex - mThumbHeroIndex + start;
+                    childIndex++) {
+                mThumbsBar.setThumbBitmap(childIndex, null);
+            }
+            for (int childIndex = heroChildIndex + end - mThumbHeroIndex + 1;
+                    childIndex < totalNum; childIndex++) {
+                mThumbsBar.setThumbBitmap(childIndex, null);
+            }
+        }
+
+        PlaybackSeekDataProvider.ResultCallback mThumbResult =
+                new PlaybackSeekDataProvider.ResultCallback() {
+                    @Override
+                    public void onThumbnailLoaded(Bitmap bitmap, int index) {
+                        int childIndex = index - (mThumbHeroIndex - mThumbsBar.getChildCount() / 2);
+                        if (childIndex < 0 || childIndex >= mThumbsBar.getChildCount()) {
+                            return;
+                        }
+                        mThumbsBar.setThumbBitmap(childIndex, bitmap);
+                    }
+        };
+
+        boolean onForward() {
+            if (!startSeek()) {
+                return false;
+            }
+            updateProgressInSeek(true);
+            return true;
+        }
+
+        boolean onBackward() {
+            if (!startSeek()) {
+                return false;
+            }
+            updateProgressInSeek(false);
+            return true;
+        }
+        /**
+         * Constructor of ViewHolder of PlaybackTransportRowPresenter
+         * @param rootView Root view of the ViewHolder.
+         * @param descriptionPresenter The presenter that will be used to create description
+         *                             ViewHolder. The description view will be added into tree.
+         */
+        public ViewHolder(View rootView, Presenter descriptionPresenter) {
+            super(rootView);
+            mImageView = (ImageView) rootView.findViewById(R.id.image);
+            mDescriptionDock = (ViewGroup) rootView.findViewById(R.id.description_dock);
+            mCurrentTime = (TextView) rootView.findViewById(R.id.current_time);
+            mTotalTime = (TextView) rootView.findViewById(R.id.total_time);
+            mProgressBar = (SeekBar) rootView.findViewById(R.id.playback_progress);
+            mProgressBar.setOnClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View view) {
+                    onProgressBarClicked(ViewHolder.this);
+                }
+            });
+            mProgressBar.setOnKeyListener(new View.OnKeyListener() {
+
+                @Override
+                public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
+                    // when in seek only allow this keys
+                    switch (keyCode) {
+                        case KeyEvent.KEYCODE_DPAD_UP:
+                        case KeyEvent.KEYCODE_DPAD_DOWN:
+                            // eat DPAD UP/DOWN in seek mode
+                            return mInSeek;
+                        case KeyEvent.KEYCODE_DPAD_LEFT:
+                        case KeyEvent.KEYCODE_MINUS:
+                        case KeyEvent.KEYCODE_MEDIA_REWIND:
+                            if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
+                                onBackward();
+                            }
+                            return true;
+                        case KeyEvent.KEYCODE_DPAD_RIGHT:
+                        case KeyEvent.KEYCODE_PLUS:
+                        case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
+                            if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
+                                onForward();
+                            }
+                            return true;
+                        case KeyEvent.KEYCODE_DPAD_CENTER:
+                        case KeyEvent.KEYCODE_ENTER:
+                            if (!mInSeek) {
+                                return false;
+                            }
+                            if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
+                                stopSeek(false);
+                            }
+                            return true;
+                        case KeyEvent.KEYCODE_BACK:
+                        case KeyEvent.KEYCODE_ESCAPE:
+                            if (!mInSeek) {
+                                return false;
+                            }
+                            if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
+                                // SeekBar does not support cancel in accessibility mode, so always
+                                // "confirm" if accessibility is on.
+                                stopSeek(Build.VERSION.SDK_INT >= 21
+                                        ? !mProgressBar.isAccessibilityFocused() : true);
+                            }
+                            return true;
+                    }
+                    return false;
+                }
+            });
+            mProgressBar.setAccessibilitySeekListener(new SeekBar.AccessibilitySeekListener() {
+                @Override
+                public boolean onAccessibilitySeekForward() {
+                    return onForward();
+                }
+
+                @Override
+                public boolean onAccessibilitySeekBackward() {
+                    return onBackward();
+                }
+            });
+            mProgressBar.setMax(Integer.MAX_VALUE); //current progress will be a fraction of this
+            mControlsDock = (ViewGroup) rootView.findViewById(R.id.controls_dock);
+            mSecondaryControlsDock =
+                    (ViewGroup) rootView.findViewById(R.id.secondary_controls_dock);
+            mDescriptionViewHolder = descriptionPresenter == null ? null :
+                    descriptionPresenter.onCreateViewHolder(mDescriptionDock);
+            if (mDescriptionViewHolder != null) {
+                mDescriptionDock.addView(mDescriptionViewHolder.view);
+            }
+            mThumbsBar = (ThumbsBar) rootView.findViewById(R.id.thumbs_row);
+        }
+
+        /**
+         * @return The ViewHolder for description.
+         */
+        public final Presenter.ViewHolder getDescriptionViewHolder() {
+            return mDescriptionViewHolder;
+        }
+
+        @Override
+        public void setPlaybackSeekUiClient(Client client) {
+            mSeekClient = client;
+        }
+
+        boolean startSeek() {
+            if (mInSeek) {
+                return true;
+            }
+            if (mSeekClient == null || !mSeekClient.isSeekEnabled()
+                    || mTotalTimeInMs <= 0) {
+                return false;
+            }
+            mInSeek = true;
+            mSeekClient.onSeekStarted();
+            mSeekDataProvider = mSeekClient.getPlaybackSeekDataProvider();
+            mPositions = mSeekDataProvider != null ? mSeekDataProvider.getSeekPositions() : null;
+            if (mPositions != null) {
+                int pos = Arrays.binarySearch(mPositions, mTotalTimeInMs);
+                if (pos >= 0) {
+                    mPositionsLength = pos + 1;
+                } else {
+                    mPositionsLength = -1 - pos;
+                }
+            } else {
+                mPositionsLength = 0;
+            }
+            mControlsVh.view.setVisibility(View.INVISIBLE);
+            mSecondaryControlsVh.view.setVisibility(View.INVISIBLE);
+            mDescriptionViewHolder.view.setVisibility(View.INVISIBLE);
+            mThumbsBar.setVisibility(View.VISIBLE);
+            return true;
+        }
+
+        void stopSeek(boolean cancelled) {
+            if (!mInSeek) {
+                return;
+            }
+            mInSeek = false;
+            mSeekClient.onSeekFinished(cancelled);
+            if (mSeekDataProvider != null) {
+                mSeekDataProvider.reset();
+            }
+            mThumbHeroIndex = -1;
+            mThumbsBar.clearThumbBitmaps();
+            mSeekDataProvider = null;
+            mPositions = null;
+            mPositionsLength = 0;
+            mControlsVh.view.setVisibility(View.VISIBLE);
+            mSecondaryControlsVh.view.setVisibility(View.VISIBLE);
+            mDescriptionViewHolder.view.setVisibility(View.VISIBLE);
+            mThumbsBar.setVisibility(View.INVISIBLE);
+        }
+
+        void dispatchItemSelection() {
+            if (!isSelected()) {
+                return;
+            }
+            if (mSelectedViewHolder == null) {
+                if (getOnItemViewSelectedListener() != null) {
+                    getOnItemViewSelectedListener().onItemSelected(null, null,
+                            ViewHolder.this, getRow());
+                }
+            } else {
+                if (getOnItemViewSelectedListener() != null) {
+                    getOnItemViewSelectedListener().onItemSelected(mSelectedViewHolder,
+                            mSelectedItem, ViewHolder.this, getRow());
+                }
+            }
+        };
+
+        Presenter getPresenter(boolean primary) {
+            ObjectAdapter adapter = primary
+                    ? ((PlaybackControlsRow) getRow()).getPrimaryActionsAdapter()
+                    : ((PlaybackControlsRow) getRow()).getSecondaryActionsAdapter();
+            if (adapter == null) {
+                return null;
+            }
+            if (adapter.getPresenterSelector() instanceof ControlButtonPresenterSelector) {
+                ControlButtonPresenterSelector selector =
+                        (ControlButtonPresenterSelector) adapter.getPresenterSelector();
+                return selector.getSecondaryPresenter();
+            }
+            return adapter.getPresenter(adapter.size() > 0 ? adapter.get(0) : null);
+        }
+
+        /**
+         * Returns the TextView that showing total time label. This method might be used in
+         * {@link #onSetDurationLabel}.
+         * @return The TextView that showing total time label.
+         */
+        public final TextView getDurationView() {
+            return mTotalTime;
+        }
+
+        /**
+         * Called to update total time label. Default implementation updates the TextView
+         * {@link #getDurationView()}. Subclass might override.
+         * @param totalTimeMs Total duration of the media in milliseconds.
+         */
+        protected void onSetDurationLabel(long totalTimeMs) {
+            if (mTotalTime != null) {
+                formatTime(totalTimeMs, mTempBuilder);
+                mTotalTime.setText(mTempBuilder.toString());
+            }
+        }
+
+        void setTotalTime(long totalTimeMs) {
+            if (mTotalTimeInMs != totalTimeMs) {
+                mTotalTimeInMs = totalTimeMs;
+                onSetDurationLabel(totalTimeMs);
+            }
+        }
+
+        /**
+         * Returns the TextView that showing current position label. This method might be used in
+         * {@link #onSetCurrentPositionLabel}.
+         * @return The TextView that showing current position label.
+         */
+        public final TextView getCurrentPositionView() {
+            return mCurrentTime;
+        }
+
+        /**
+         * Called to update current time label. Default implementation updates the TextView
+         * {@link #getCurrentPositionView}. Subclass might override.
+         * @param currentTimeMs Current playback position in milliseconds.
+         */
+        protected void onSetCurrentPositionLabel(long currentTimeMs) {
+            if (mCurrentTime != null) {
+                formatTime(currentTimeMs, mTempBuilder);
+                mCurrentTime.setText(mTempBuilder.toString());
+            }
+        }
+
+        void setCurrentPosition(long currentTimeMs) {
+            if (currentTimeMs != mCurrentTimeInMs) {
+                mCurrentTimeInMs = currentTimeMs;
+                onSetCurrentPositionLabel(currentTimeMs);
+            }
+            if (!mInSeek) {
+                int progressRatio = 0;
+                if (mTotalTimeInMs > 0) {
+                    // Use ratio to represent current progres
+                    double ratio = (double) mCurrentTimeInMs / mTotalTimeInMs;     // Range: [0, 1]
+                    progressRatio = (int) (ratio * Integer.MAX_VALUE);  // Could safely cast to int
+                }
+                mProgressBar.setProgress((int) progressRatio);
+            }
+        }
+
+        void setBufferedPosition(long progressMs) {
+            mSecondaryProgressInMs = progressMs;
+            // Solve the progress bar by using ratio
+            double ratio = (double) progressMs / mTotalTimeInMs;           // Range: [0, 1]
+            double progressRatio = ratio * Integer.MAX_VALUE;   // Could safely cast to int
+            mProgressBar.setSecondaryProgress((int) progressRatio);
+        }
+    }
+
+    static void formatTime(long ms, StringBuilder sb) {
+        sb.setLength(0);
+        if (ms < 0) {
+            sb.append("--");
+            return;
+        }
+        long seconds = ms / 1000;
+        long minutes = seconds / 60;
+        long hours = minutes / 60;
+        seconds -= minutes * 60;
+        minutes -= hours * 60;
+
+        if (hours > 0) {
+            sb.append(hours).append(':');
+            if (minutes < 10) {
+                sb.append('0');
+            }
+        }
+        sb.append(minutes).append(':');
+        if (seconds < 10) {
+            sb.append('0');
+        }
+        sb.append(seconds);
+    }
+
+    float mDefaultSeekIncrement = 0.01f;
+    int mProgressColor = Color.TRANSPARENT;
+    boolean mProgressColorSet;
+    Presenter mDescriptionPresenter;
+    ControlBarPresenter mPlaybackControlsPresenter;
+    ControlBarPresenter mSecondaryControlsPresenter;
+    OnActionClickedListener mOnActionClickedListener;
+
+    private final OnControlSelectedListener mOnControlSelectedListener =
+            new OnControlSelectedListener() {
+        @Override
+        public void onControlSelected(Presenter.ViewHolder itemViewHolder, Object item,
+                ControlBarPresenter.BoundData data) {
+            ViewHolder vh = ((BoundData) data).mRowViewHolder;
+            if (vh.mSelectedViewHolder != itemViewHolder || vh.mSelectedItem != item) {
+                vh.mSelectedViewHolder = itemViewHolder;
+                vh.mSelectedItem = item;
+                vh.dispatchItemSelection();
+            }
+        }
+    };
+
+    private final OnControlClickedListener mOnControlClickedListener =
+            new OnControlClickedListener() {
+        @Override
+        public void onControlClicked(Presenter.ViewHolder itemViewHolder, Object item,
+                ControlBarPresenter.BoundData data) {
+            ViewHolder vh = ((BoundData) data).mRowViewHolder;
+            if (vh.getOnItemViewClickedListener() != null) {
+                vh.getOnItemViewClickedListener().onItemClicked(itemViewHolder, item,
+                        vh, vh.getRow());
+            }
+            if (mOnActionClickedListener != null && item instanceof Action) {
+                mOnActionClickedListener.onActionClicked((Action) item);
+            }
+        }
+    };
+
+    public PlaybackTransportRowPresenter() {
+        setHeaderPresenter(null);
+        setSelectEffectEnabled(false);
+
+        mPlaybackControlsPresenter = new ControlBarPresenter(R.layout.lb_control_bar);
+        mPlaybackControlsPresenter.setDefaultFocusToMiddle(false);
+        mSecondaryControlsPresenter = new ControlBarPresenter(R.layout.lb_control_bar);
+        mSecondaryControlsPresenter.setDefaultFocusToMiddle(false);
+
+        mPlaybackControlsPresenter.setOnControlSelectedListener(mOnControlSelectedListener);
+        mSecondaryControlsPresenter.setOnControlSelectedListener(mOnControlSelectedListener);
+        mPlaybackControlsPresenter.setOnControlClickedListener(mOnControlClickedListener);
+        mSecondaryControlsPresenter.setOnControlClickedListener(mOnControlClickedListener);
+    }
+
+    /**
+     * @param descriptionPresenter Presenter for displaying item details.
+     */
+    public void setDescriptionPresenter(Presenter descriptionPresenter) {
+        mDescriptionPresenter = descriptionPresenter;
+    }
+
+    /**
+     * Sets the listener for {@link Action} click events.
+     */
+    public void setOnActionClickedListener(OnActionClickedListener listener) {
+        mOnActionClickedListener = listener;
+    }
+
+    /**
+     * Returns the listener for {@link Action} click events.
+     */
+    public OnActionClickedListener getOnActionClickedListener() {
+        return mOnActionClickedListener;
+    }
+
+    /**
+     * Sets the primary color for the progress bar.  If not set, a default from
+     * the theme will be used.
+     */
+    public void setProgressColor(@ColorInt int color) {
+        mProgressColor = color;
+        mProgressColorSet = true;
+    }
+
+    /**
+     * Returns the primary color for the progress bar.  If no color was set, transparent
+     * is returned.
+     */
+    @ColorInt
+    public int getProgressColor() {
+        return mProgressColor;
+    }
+
+    @Override
+    public void onReappear(RowPresenter.ViewHolder rowViewHolder) {
+        ViewHolder vh = (ViewHolder) rowViewHolder;
+        if (vh.view.hasFocus()) {
+            vh.mProgressBar.requestFocus();
+        }
+    }
+
+    private int getDefaultProgressColor(Context context) {
+        TypedValue outValue = new TypedValue();
+        if (context.getTheme()
+                .resolveAttribute(R.attr.playbackProgressPrimaryColor, outValue, true)) {
+            return context.getResources().getColor(outValue.resourceId);
+        }
+        return context.getResources().getColor(R.color.lb_playback_progress_color_no_theme);
+    }
+
+    @Override
+    protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) {
+        View v = LayoutInflater.from(parent.getContext()).inflate(
+                R.layout.lb_playback_transport_controls_row, parent, false);
+        ViewHolder vh = new ViewHolder(v, mDescriptionPresenter);
+        initRow(vh);
+        return vh;
+    }
+
+    private void initRow(final ViewHolder vh) {
+        vh.mControlsVh = (ControlBarPresenter.ViewHolder) mPlaybackControlsPresenter
+                .onCreateViewHolder(vh.mControlsDock);
+        vh.mProgressBar.setProgressColor(mProgressColorSet ? mProgressColor
+                : getDefaultProgressColor(vh.mControlsDock.getContext()));
+        vh.mControlsDock.addView(vh.mControlsVh.view);
+
+        vh.mSecondaryControlsVh = (ControlBarPresenter.ViewHolder) mSecondaryControlsPresenter
+                .onCreateViewHolder(vh.mSecondaryControlsDock);
+        vh.mSecondaryControlsDock.addView(vh.mSecondaryControlsVh.view);
+        ((PlaybackTransportRowView) vh.view).setOnUnhandledKeyListener(
+                new PlaybackTransportRowView.OnUnhandledKeyListener() {
+                @Override
+                public boolean onUnhandledKey(KeyEvent event) {
+                    if (vh.getOnKeyListener() != null) {
+                        if (vh.getOnKeyListener().onKey(vh.view, event.getKeyCode(), event)) {
+                            return true;
+                        }
+                    }
+                    return false;
+                }
+            });
+    }
+
+    @Override
+    protected void onBindRowViewHolder(RowPresenter.ViewHolder holder, Object item) {
+        super.onBindRowViewHolder(holder, item);
+
+        ViewHolder vh = (ViewHolder) holder;
+        PlaybackControlsRow row = (PlaybackControlsRow) vh.getRow();
+
+        if (row.getItem() == null) {
+            vh.mDescriptionDock.setVisibility(View.GONE);
+        } else {
+            vh.mDescriptionDock.setVisibility(View.VISIBLE);
+            if (vh.mDescriptionViewHolder != null) {
+                mDescriptionPresenter.onBindViewHolder(vh.mDescriptionViewHolder, row.getItem());
+            }
+        }
+
+        if (row.getImageDrawable() == null) {
+            vh.mImageView.setVisibility(View.GONE);
+        } else {
+            vh.mImageView.setVisibility(View.VISIBLE);
+        }
+        vh.mImageView.setImageDrawable(row.getImageDrawable());
+
+        vh.mControlsBoundData.adapter = row.getPrimaryActionsAdapter();
+        vh.mControlsBoundData.presenter = vh.getPresenter(true);
+        vh.mControlsBoundData.mRowViewHolder = vh;
+        mPlaybackControlsPresenter.onBindViewHolder(vh.mControlsVh, vh.mControlsBoundData);
+
+        vh.mSecondaryBoundData.adapter = row.getSecondaryActionsAdapter();
+        vh.mSecondaryBoundData.presenter = vh.getPresenter(false);
+        vh.mSecondaryBoundData.mRowViewHolder = vh;
+        mSecondaryControlsPresenter.onBindViewHolder(vh.mSecondaryControlsVh,
+                vh.mSecondaryBoundData);
+
+        vh.setTotalTime(row.getDuration());
+        vh.setCurrentPosition(row.getCurrentPosition());
+        vh.setBufferedPosition(row.getBufferedPosition());
+        row.setOnPlaybackProgressChangedListener(vh.mListener);
+    }
+
+    @Override
+    protected void onUnbindRowViewHolder(RowPresenter.ViewHolder holder) {
+        ViewHolder vh = (ViewHolder) holder;
+        PlaybackControlsRow row = (PlaybackControlsRow) vh.getRow();
+
+        if (vh.mDescriptionViewHolder != null) {
+            mDescriptionPresenter.onUnbindViewHolder(vh.mDescriptionViewHolder);
+        }
+        mPlaybackControlsPresenter.onUnbindViewHolder(vh.mControlsVh);
+        mSecondaryControlsPresenter.onUnbindViewHolder(vh.mSecondaryControlsVh);
+        row.setOnPlaybackProgressChangedListener(null);
+
+        super.onUnbindRowViewHolder(holder);
+    }
+
+    /**
+     * Client of progress bar is clicked, default implementation delegate click to
+     * PlayPauseAction.
+     *
+     * @param vh ViewHolder of PlaybackTransportRowPresenter
+     */
+    protected void onProgressBarClicked(ViewHolder vh) {
+        if (vh != null) {
+            if (vh.mPlayPauseAction == null) {
+                vh.mPlayPauseAction = new PlaybackControlsRow.PlayPauseAction(vh.view.getContext());
+            }
+            if (vh.getOnItemViewClickedListener() != null) {
+                vh.getOnItemViewClickedListener().onItemClicked(vh, vh.mPlayPauseAction,
+                        vh, vh.getRow());
+            }
+            if (mOnActionClickedListener != null) {
+                mOnActionClickedListener.onActionClicked(vh.mPlayPauseAction);
+            }
+        }
+    }
+
+    /**
+     * Set default seek increment if {@link PlaybackSeekDataProvider} is null.
+     * @param ratio float value between 0(inclusive) and 1(inclusive).
+     */
+    public void setDefaultSeekIncrement(float ratio) {
+        mDefaultSeekIncrement = ratio;
+    }
+
+    /**
+     * Get default seek increment if {@link PlaybackSeekDataProvider} is null.
+     * @return float value between 0(inclusive) and 1(inclusive).
+     */
+    public float getDefaultSeekIncrement() {
+        return mDefaultSeekIncrement;
+    }
+
+    @Override
+    protected void onRowViewSelected(RowPresenter.ViewHolder vh, boolean selected) {
+        super.onRowViewSelected(vh, selected);
+        if (selected) {
+            ((ViewHolder) vh).dispatchItemSelection();
+        }
+    }
+
+    @Override
+    protected void onRowViewAttachedToWindow(RowPresenter.ViewHolder vh) {
+        super.onRowViewAttachedToWindow(vh);
+        if (mDescriptionPresenter != null) {
+            mDescriptionPresenter.onViewAttachedToWindow(
+                    ((ViewHolder) vh).mDescriptionViewHolder);
+        }
+    }
+
+    @Override
+    protected void onRowViewDetachedFromWindow(RowPresenter.ViewHolder vh) {
+        super.onRowViewDetachedFromWindow(vh);
+        if (mDescriptionPresenter != null) {
+            mDescriptionPresenter.onViewDetachedFromWindow(
+                    ((ViewHolder) vh).mDescriptionViewHolder);
+        }
+    }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackTransportRowView.java b/v17/leanback/src/android/support/v17/leanback/widget/PlaybackTransportRowView.java
new file mode 100644
index 0000000..2af7ff4
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/PlaybackTransportRowView.java
@@ -0,0 +1,125 @@
+/*
+ * 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 android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.support.annotation.RestrictTo;
+import android.support.v17.leanback.R;
+import android.util.AttributeSet;
+import android.view.FocusFinder;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+/**
+ * View for PlaybackTransportRowPresenter that has a custom focusSearch.
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public class PlaybackTransportRowView extends LinearLayout {
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public interface OnUnhandledKeyListener {
+        /**
+         * Returns true if the key event should be consumed.
+         */
+        boolean onUnhandledKey(KeyEvent event);
+    }
+
+    private OnUnhandledKeyListener mOnUnhandledKeyListener;
+
+    public PlaybackTransportRowView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public PlaybackTransportRowView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    void setOnUnhandledKeyListener(OnUnhandledKeyListener listener) {
+        mOnUnhandledKeyListener = listener;
+    }
+
+    OnUnhandledKeyListener getOnUnhandledKeyListener() {
+        return mOnUnhandledKeyListener;
+    }
+
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        if (super.dispatchKeyEvent(event)) {
+            return true;
+        }
+        return mOnUnhandledKeyListener != null && mOnUnhandledKeyListener.onUnhandledKey(event);
+    }
+
+    @Override
+    protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
+        final View focused = findFocus();
+        if (focused != null && focused.requestFocus(direction, previouslyFocusedRect)) {
+            return true;
+        }
+        View progress = findViewById(R.id.playback_progress);
+        if (progress != null && progress.isFocusable()) {
+            if (progress.requestFocus(direction, previouslyFocusedRect)) {
+                return true;
+            }
+        }
+        return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
+    }
+
+    @Override
+    public View focusSearch(View focused, int direction) {
+        // when focusSearch vertically, return the next immediate focusable child
+        if (focused != null) {
+            if (direction == View.FOCUS_UP) {
+                int index = indexOfChild(getFocusedChild());
+                for (index = index - 1; index >= 0; index--) {
+                    View view = getChildAt(index);
+                    if (view.hasFocusable()) {
+                        return view;
+                    }
+                }
+            } else if (direction == View.FOCUS_DOWN) {
+                int index = indexOfChild(getFocusedChild());
+                for (index = index + 1; index < getChildCount(); index++) {
+                    View view = getChildAt(index);
+                    if (view.hasFocusable()) {
+                        return view;
+                    }
+                }
+            } else if (direction == View.FOCUS_LEFT || direction == View.FOCUS_RIGHT) {
+                if (getFocusedChild() instanceof ViewGroup) {
+                    return FocusFinder.getInstance().findNextFocus(
+                            (ViewGroup) getFocusedChild(), focused, direction);
+                }
+            }
+        }
+        return super.focusSearch(focused, direction);
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/RowContainerView.java b/v17/leanback/src/android/support/v17/leanback/widget/RowContainerView.java
index b845bb7..dffcbb5 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/RowContainerView.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/RowContainerView.java
@@ -110,4 +110,9 @@
             mForeground.draw(canvas);
         }
     }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SeekBar.java b/v17/leanback/src/android/support/v17/leanback/widget/SeekBar.java
new file mode 100644
index 0000000..b86b9be
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/SeekBar.java
@@ -0,0 +1,246 @@
+/*
+ * 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 android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Bundle;
+import android.support.annotation.RestrictTo;
+import android.support.v17.leanback.R;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.util.AttributeSet;
+import android.view.View;
+
+/**
+ * Replacement of SeekBar, has two bar heights and two thumb size when focused/not_focused.
+ * The widget does not deal with KeyEvent, it's client's responsibility to set a key listener.
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public final class SeekBar extends View {
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public abstract static class AccessibilitySeekListener {
+        /**
+         * Called to perform AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD
+         */
+        public abstract boolean onAccessibilitySeekForward();
+        /**
+         * Called to perform AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD
+         */
+        public abstract boolean onAccessibilitySeekBackward();
+    }
+
+    private final RectF mProgressRect = new RectF();
+    private final RectF mSecondProgressRect = new RectF();
+    private final RectF mBackgroundRect = new RectF();
+    private final Paint mSecondProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+    private final Paint mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+    private final Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+    private final Paint mKnobPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+    private int mProgress;
+    private int mSecondProgress;
+    private int mMax;
+    private int mKnobx;
+
+    private int mActiveRadius;
+    private int mBarHeight;
+    private int mActiveBarHeight;
+
+    private AccessibilitySeekListener mAccessibilitySeekListener;
+
+    public SeekBar(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        setWillNotDraw(false);
+        mBackgroundPaint.setColor(Color.GRAY);
+        mSecondProgressPaint.setColor(Color.LTGRAY);
+        mProgressPaint.setColor(Color.RED);
+        mKnobPaint.setColor(Color.WHITE);
+        mBarHeight = context.getResources().getDimensionPixelSize(
+                R.dimen.lb_playback_transport_progressbar_bar_height);
+        mActiveBarHeight = context.getResources().getDimensionPixelSize(
+                R.dimen.lb_playback_transport_progressbar_active_bar_height);
+        mActiveRadius = context.getResources().getDimensionPixelSize(
+                R.dimen.lb_playback_transport_progressbar_active_radius);
+    }
+
+    /**
+     * Set radius in pixels for thumb when SeekBar is focused.
+     */
+    public void setActiveRadius(int radius) {
+        mActiveRadius = radius;
+        calculate();
+    }
+
+    /**
+     * Set horizontal bar height in pixels when SeekBar is not focused.
+     */
+    public void setBarHeight(int barHeight) {
+        mBarHeight = barHeight;
+        calculate();
+    }
+
+    /**
+     * Set horizontal bar height in pixels when SeekBar is focused.
+     */
+    public void setActiveBarHeight(int activeBarHeight) {
+        mActiveBarHeight = activeBarHeight;
+        calculate();
+    }
+
+    @Override
+    protected void onFocusChanged(boolean gainFocus,
+            int direction, Rect previouslyFocusedRect) {
+        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
+        calculate();
+    }
+
+    @Override
+    protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+        calculate();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        final int radius = isFocused() ? mActiveRadius : mBarHeight / 2;
+        canvas.drawRoundRect(mBackgroundRect, radius, radius, mBackgroundPaint);
+        canvas.drawRoundRect(mSecondProgressRect, radius, radius, mProgressPaint);
+        canvas.drawRoundRect(mProgressRect, radius, radius, mProgressPaint);
+        canvas.drawCircle(mKnobx, getHeight() / 2, radius, mKnobPaint);
+    }
+
+    /**
+     * Set progress within 0 and {@link #getMax()}
+     */
+    public void setProgress(int progress) {
+        if (progress > mMax) {
+            progress = mMax;
+        } else if (progress < 0) {
+            progress = 0;
+        }
+        mProgress = progress;
+        calculate();
+    }
+
+    /**
+     * Set secondary progress within 0 and {@link #getMax()}
+     */
+    public void setSecondaryProgress(int progress) {
+        if (progress > mMax) {
+            progress = mMax;
+        } else if (progress < 0) {
+            progress = 0;
+        }
+        mSecondProgress = progress;
+        calculate();
+    }
+
+    /**
+     * Get progress within 0 and {@link #getMax()}
+     */
+    public int getProgress() {
+        return mProgress;
+    }
+
+    /**
+     * Get secondary progress within 0 and {@link #getMax()}
+     */
+    public int getSecondProgress() {
+        return mSecondProgress;
+    }
+
+    /**
+     * Get max value.
+     */
+    public int getMax() {
+        return mMax;
+    }
+
+    /**
+     * Set max value.
+     */
+    public void setMax(int max) {
+        this.mMax = max;
+        calculate();
+    }
+
+    /**
+     * Set color for progress.
+     */
+    public void setProgressColor(int color) {
+        mProgressPaint.setColor(color);
+    }
+
+    private void calculate() {
+        final int barHeight = isFocused() ? mActiveBarHeight : mBarHeight;
+
+        final int width = getWidth();
+        final int height = getHeight();
+        final int verticalPadding = (height - barHeight) / 2;
+
+        mBackgroundRect.set(mBarHeight / 2, verticalPadding,
+                width - mBarHeight / 2, height - verticalPadding);
+
+        final int radius = isFocused() ? mActiveRadius : mBarHeight / 2;
+        final int progressWidth = width - radius * 2;
+        final float progressPixels = mProgress / (float) mMax * progressWidth;
+        mProgressRect.set(mBarHeight / 2, verticalPadding, mBarHeight / 2 + progressPixels,
+                height - verticalPadding);
+
+        final float secondProgressPixels = mSecondProgress / (float) mMax * progressWidth;
+        mSecondProgressRect.set(mBarHeight / 2, verticalPadding,
+                mBarHeight / 2 + secondProgressPixels, height - verticalPadding);
+
+        mKnobx = radius + (int) progressPixels;
+        invalidate();
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return android.widget.SeekBar.class.getName();
+    }
+
+    public void setAccessibilitySeekListener(AccessibilitySeekListener listener) {
+        mAccessibilitySeekListener = listener;
+    }
+
+    @Override
+    public boolean performAccessibilityAction(int action, Bundle arguments) {
+        if (mAccessibilitySeekListener != null) {
+            switch (action) {
+                case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD:
+                    return mAccessibilitySeekListener.onAccessibilitySeekForward();
+                case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD:
+                    return mAccessibilitySeekListener.onAccessibilitySeekBackward();
+            }
+        }
+        return super.performAccessibilityAction(action, arguments);
+    }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ThumbsBar.java b/v17/leanback/src/android/support/v17/leanback/widget/ThumbsBar.java
new file mode 100644
index 0000000..85dde7f
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ThumbsBar.java
@@ -0,0 +1,231 @@
+/*
+ * 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 android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.support.annotation.RestrictTo;
+import android.util.AttributeSet;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+/**
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public class ThumbsBar extends LinearLayout {
+
+    static final int DEFAULT_NUM_OF_THUMBS = 7;
+
+    int mMinimalMargin = 16;
+    int mNumOfThumbs;
+    int mThumbWidth = 160;
+    int mThumbHeight = 160;
+    int mHeroThumbWidth = 240;
+    int mHeroThumbHeight = 240;
+    int mMeasuredMargin;
+    final SparseArray<Bitmap> mBitmaps = new SparseArray<>();
+
+    public ThumbsBar(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public ThumbsBar(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        setNumberOfThumbs(DEFAULT_NUM_OF_THUMBS);
+    }
+
+    /**
+     * Get hero index which is the middle child.
+     */
+    public int getHeroIndex() {
+        return getChildCount() / 2;
+    }
+
+    /**
+     * Set size of thumb view in pixels
+     */
+    public void setThumbSize(int width, int height) {
+        mThumbHeight = height;
+        mThumbWidth = width;
+        int heroIndex = getHeroIndex();
+        for (int i = 0; i < getChildCount(); i++) {
+            if (heroIndex != i) {
+                View child = getChildAt(i);
+                LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
+                boolean changed = false;
+                if (lp.height != height) {
+                    lp.height = height;
+                    changed = true;
+                }
+                if (lp.width != width) {
+                    lp.width = width;
+                    changed = true;
+                }
+                if (changed) {
+                    child.setLayoutParams(lp);
+                }
+            }
+        }
+    }
+
+    /**
+     * Set size of hero thumb view in pixels, it is usually larger than other thumbs.
+     */
+    public void setHeroThumbSize(int width, int height) {
+        mHeroThumbHeight = height;
+        mHeroThumbWidth = width;
+        int heroIndex = getHeroIndex();
+        for (int i = 0; i < getChildCount(); i++) {
+            if (heroIndex == i) {
+                View child = getChildAt(i);
+                LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
+                boolean changed = false;
+                if (lp.height != height) {
+                    lp.height = height;
+                    changed = true;
+                }
+                if (lp.width != width) {
+                    lp.width = width;
+                    changed = true;
+                }
+                if (changed) {
+                    child.setLayoutParams(lp);
+                }
+            }
+        }
+    }
+
+    /**
+     * Set number of thumb views. It must be odd or it will be increasing one.
+     */
+    public void setNumberOfThumbs(int numOfThumbs) {
+        if (numOfThumbs < 0) {
+            throw new IllegalArgumentException();
+        }
+        if ((numOfThumbs & 1) == 0) {
+            // make it odd number
+            numOfThumbs++;
+        }
+        mNumOfThumbs = numOfThumbs;
+        while (getChildCount() > mNumOfThumbs) {
+            removeView(getChildAt(getChildCount() - 1));
+        }
+        while (getChildCount() < mNumOfThumbs) {
+            View view = createThumbView(this);
+            LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(mThumbWidth, mThumbHeight);
+            addView(view, lp);
+        }
+        int heroIndex = getHeroIndex();
+        for (int i = 0; i < getChildCount(); i++) {
+            View child = getChildAt(i);
+            LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
+            if (heroIndex == i) {
+                lp.width = mHeroThumbWidth;
+                lp.height = mHeroThumbHeight;
+            } else {
+                lp.width = mThumbWidth;
+                lp.height = mThumbHeight;
+            }
+            child.setLayoutParams(lp);
+        }
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        int width = getMeasuredWidth();
+        int spaceForMargin = 0;
+        while (mNumOfThumbs > 1) {
+            spaceForMargin = width - mHeroThumbWidth - mThumbWidth * (mNumOfThumbs - 1);
+            if (spaceForMargin < mMinimalMargin * (mNumOfThumbs - 1)) {
+                setNumberOfThumbs(mNumOfThumbs - 2);
+            } else {
+                break;
+            }
+        }
+        mMeasuredMargin = mNumOfThumbs > 0 ? spaceForMargin / (mNumOfThumbs - 1) : 0;
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+        int heroIndex = getHeroIndex();
+        View heroView = getChildAt(heroIndex);
+        int heroLeft = getWidth() / 2 - heroView.getMeasuredWidth() / 2;
+        int heroRight = getWidth() / 2 + heroView.getMeasuredWidth() / 2;
+        heroView.layout(heroLeft, getPaddingTop(), heroRight,
+                getPaddingTop() + heroView.getMeasuredHeight());
+        int heroCenter = getPaddingTop() + heroView.getMeasuredHeight() / 2;
+
+        for (int i = heroIndex - 1; i >= 0; i--) {
+            heroLeft -= mMeasuredMargin;
+            View child = getChildAt(i);
+            child.layout(heroLeft - child.getMeasuredWidth(),
+                    heroCenter - child.getMeasuredHeight() / 2,
+                    heroLeft,
+                    heroCenter + child.getMeasuredHeight() / 2);
+            heroLeft -= child.getMeasuredWidth();
+        }
+        for (int i = heroIndex + 1; i < mNumOfThumbs; i++) {
+            heroRight += mMeasuredMargin;
+            View child = getChildAt(i);
+            child.layout(heroRight,
+                    heroCenter - child.getMeasuredHeight() / 2,
+                    heroRight + child.getMeasuredWidth(),
+                    heroCenter + child.getMeasuredHeight() / 2);
+            heroRight += child.getMeasuredWidth();
+        }
+    }
+
+    /**
+     * Create a thumb view, it's by default a ImageView.
+     */
+    protected View createThumbView(ViewGroup parent) {
+        return new ImageView(parent.getContext());
+    }
+
+    /**
+     * Clear all thumb bitmaps set on thumb views.
+     */
+    public void clearThumbBitmaps() {
+        for (int i = 0; i < getChildCount(); i++) {
+            setThumbBitmap(i, null);
+        }
+        mBitmaps.clear();
+    }
+
+
+    /**
+     * Get bitmap of given child index.
+     */
+    public Bitmap getThumbBitmap(int index) {
+        return mBitmaps.get(index);
+    }
+
+    /**
+     * Set thumb bitmap for a given index of child.
+     */
+    public void setThumbBitmap(int index, Bitmap bitmap) {
+        mBitmaps.put(index, bitmap);
+        ((ImageView) getChildAt(index)).setImageBitmap(bitmap);
+    }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java
index 2c361f4..727df09 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java
@@ -806,7 +806,7 @@
             }
         });
 
-        // wait a little bit then clear glue
+        // wait a little bit then reset glue
         SystemClock.sleep(1000);
         InstrumentationRegistry.getInstrumentation().runOnMainSync(
                 new Runnable() {
@@ -816,7 +816,7 @@
                     }
                 }
         );
-        // background should fade in upon clear playback
+        // background should fade in upon reset playback
         PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
             @Override
             public boolean canProceed() {
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsSupportFragmentTest.java
index b4322ec..1e01d45 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsSupportFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsSupportFragmentTest.java
@@ -809,7 +809,7 @@
             }
         });
 
-        // wait a little bit then clear glue
+        // wait a little bit then reset glue
         SystemClock.sleep(1000);
         InstrumentationRegistry.getInstrumentation().runOnMainSync(
                 new Runnable() {
@@ -819,7 +819,7 @@
                     }
                 }
         );
-        // background should fade in upon clear playback
+        // background should fade in upon reset playback
         PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
             @Override
             public boolean canProceed() {
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 de8ff88..c732191 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
@@ -104,11 +104,11 @@
         mGlue = new PlaybackControlHelper(context, this) {
             @Override
             public int getUpdatePeriod() {
-                int totalTime = getControlsRow().getTotalTime();
+                long totalTime = getControlsRow().getDuration();
                 if (getView() == null || getView().getWidth() == 0 || totalTime <= 0) {
                     return 1000;
                 }
-                return Math.max(16, totalTime / getView().getWidth());
+                return 16;
             }
 
             @Override
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 c9cc5b2..9380760 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
@@ -94,11 +94,11 @@
         mGlue = new PlaybackControlHelper(context) {
             @Override
             public int getUpdatePeriod() {
-                int totalTime = getControlsRow().getTotalTime();
+                long totalTime = getControlsRow().getDuration();
                 if (getView() == null || getView().getWidth() == 0 || totalTime <= 0) {
                     return 1000;
                 }
-                return Math.max(16, totalTime / getView().getWidth());
+                return 16;
             }
 
             @Override
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestSupportFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestSupportFragment.java
index 4e63522..ddf0405 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestSupportFragment.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestSupportFragment.java
@@ -97,11 +97,11 @@
         mGlue = new PlaybackControlHelper(context) {
             @Override
             public int getUpdatePeriod() {
-                int totalTime = getControlsRow().getTotalTime();
+                long totalTime = getControlsRow().getDuration();
                 if (getView() == null || getView().getWidth() == 0 || totalTime <= 0) {
                     return 1000;
                 }
-                return Math.max(16, totalTime / getView().getWidth());
+                return 16;
             }
 
             @Override
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 f15521a..ba5d68e 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
@@ -163,11 +163,13 @@
             mGlue.setTitle("Leanback team at work");
             mGlue.setMediaSource(
                     Uri.parse("android.resource://android.support.v17.leanback.test/raw/video"));
-            mGlue.setPlayerCallback(new PlaybackGlue.PlayerCallback() {
+            mGlue.addPlayerCallback(new PlaybackGlue.PlayerCallback() {
                 @Override
-                public void onReadyForPlayback() {
-                    mGlueOnReadyForPlaybackCalled++;
-                    mGlue.play();
+                public void onPreparedStateChanged(PlaybackGlue glue) {
+                    if (glue.isPrepared()) {
+                        mGlueOnReadyForPlaybackCalled++;
+                        mGlue.play();
+                    }
                 }
             });
             mGlue.setHost(new VideoFragmentGlueHost(this));
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
index 8bca7fc..457268d 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoSupportFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/VideoSupportFragmentTest.java
@@ -166,11 +166,13 @@
             mGlue.setTitle("Leanback team at work");
             mGlue.setMediaSource(
                     Uri.parse("android.resource://android.support.v17.leanback.test/raw/video"));
-            mGlue.setPlayerCallback(new PlaybackGlue.PlayerCallback() {
+            mGlue.addPlayerCallback(new PlaybackGlue.PlayerCallback() {
                 @Override
-                public void onReadyForPlayback() {
-                    mGlueOnReadyForPlaybackCalled++;
-                    mGlue.play();
+                public void onPreparedStateChanged(PlaybackGlue glue) {
+                    if (glue.isPrepared()) {
+                        mGlueOnReadyForPlaybackCalled++;
+                        mGlue.play();
+                    }
                 }
             });
             mGlue.setHost(new VideoSupportFragmentGlueHost(this));
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
index a2956d6..c1f1807 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/media/MediaPlayerGlueTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/media/MediaPlayerGlueTest.java
@@ -84,7 +84,7 @@
             }
         });
 
-        // Test enableProgressUpdating(true) and enableProgressUpdating(false);
+        // Test setProgressUpdatingEnabled(true) and setProgressUpdatingEnabled(false);
         InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
             @Override
             public void run() {
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
index 2c9aa43..96a1342 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueHostImpl.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueHostImpl.java
@@ -23,9 +23,9 @@
  */
 public class PlaybackGlueHostImpl extends PlaybackGlueHost {
 
-    HostCallback mHostCallback;
-    Row mRow;
-    PlaybackRowPresenter mPlaybackRowPresenter;
+    protected HostCallback mHostCallback;
+    protected Row mRow;
+    protected PlaybackRowPresenter mPlaybackRowPresenter;
 
     @Override
     public void setHostCallback(HostCallback callback) {
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
index 3932ea6..0f96196 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueTest.java
@@ -16,8 +16,10 @@
 
 package android.support.v17.leanback.media;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.times;
 
 import android.content.Context;
@@ -78,4 +80,38 @@
         Mockito.verify(glue2, times(1)).onDetachedFromHost();
     }
 
+    @Test
+    public void listenerModification() {
+        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        PlaybackGlue glue = Mockito.spy(new PlaybackGlueImpl(context));
+        PlaybackGlueHostImpl host = new PlaybackGlueHostImpl();
+
+        glue.setHost(host);
+        final boolean[] called = new boolean[] {false, false};
+        glue.addPlayerCallback(new PlaybackGlue.PlayerCallback() {
+            @Override
+            public void onPreparedStateChanged(PlaybackGlue glue) {
+                called[0] = true;
+            }
+        });
+        glue.addPlayerCallback(new PlaybackGlue.PlayerCallback() {
+            @Override
+            public void onPreparedStateChanged(PlaybackGlue glue) {
+                glue.removePlayerCallback(this);
+            }
+        });
+        glue.addPlayerCallback(new PlaybackGlue.PlayerCallback() {
+            @Override
+            public void onPreparedStateChanged(PlaybackGlue glue) {
+                called[1] = true;
+            }
+        });
+
+        for (PlaybackGlue.PlayerCallback callback: glue.getPlayerCallbacks()) {
+            callback.onPreparedStateChanged(glue);
+        }
+        assertTrue(called[0]);
+        assertTrue(called[1]);
+        assertEquals(2, glue.getPlayerCallbacks().size());
+    }
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackTransportControlGlueTest.java b/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackTransportControlGlueTest.java
new file mode 100644
index 0000000..2400756
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackTransportControlGlueTest.java
@@ -0,0 +1,216 @@
+/*
+ * 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 junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.v17.leanback.widget.PlaybackControlsRow;
+import android.support.v17.leanback.widget.PlaybackRowPresenter;
+import android.support.v17.leanback.widget.PlaybackTransportRowPresenter;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.view.ContextThemeWrapper;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+import org.junit.Test;
+import org.mockito.Mockito;
+
+@SmallTest
+public class PlaybackTransportControlGlueTest {
+
+    public static class PlayerAdapterSample extends PlayerAdapter {
+        @Override
+        public void play() {
+        }
+
+        @Override
+        public void pause() {
+        }
+    }
+
+    public static class PlaybackTransportControlGlueImpl
+            extends PlaybackTransportControlGlue {
+        public PlaybackTransportControlGlueImpl(Context context) {
+            super(context, new PlayerAdapterSample());
+        }
+
+        public PlaybackTransportControlGlueImpl(Context context, PlayerAdapter impl) {
+            super(context, impl);
+        }
+    }
+
+    Context mContext;
+    PlaybackTransportControlGlueImpl mGlue;
+    PlaybackTransportRowPresenter.ViewHolder mViewHolder;
+
+    @Test
+    public void usingDefaultRowAndPresenter() {
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mGlue = new PlaybackTransportControlGlueImpl(mContext);
+            }
+        });
+        PlaybackGlueHostImpl host = new PlaybackGlueHostImpl();
+
+        mGlue.setHost(host);
+        assertSame(mGlue, host.mGlue);
+        assertSame(host, mGlue.getHost());
+        assertTrue(host.mPlaybackRowPresenter instanceof PlaybackTransportRowPresenter);
+        assertTrue(host.mRow instanceof PlaybackControlsRow);
+
+    }
+    @Test
+    public void customRowPresenter() {
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mGlue = new PlaybackTransportControlGlueImpl(mContext);
+            }
+        });
+        PlaybackRowPresenter presenter = new PlaybackRowPresenter() {
+            @Override
+            protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) {
+                return new RowPresenter.ViewHolder(new LinearLayout(parent.getContext()));
+            }
+        };
+        mGlue.setPlaybackRowPresenter(presenter);
+        PlaybackGlueHostImpl host = new PlaybackGlueHostImpl();
+
+        mGlue.setHost(host);
+        assertSame(mGlue, host.mGlue);
+        assertSame(host, mGlue.getHost());
+        assertSame(host.mPlaybackRowPresenter, presenter);
+        assertTrue(host.mRow instanceof PlaybackControlsRow);
+
+    }
+
+    @Test
+    public void customControlsRow() {
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mGlue = new PlaybackTransportControlGlueImpl(mContext);
+            }
+        });
+        PlaybackControlsRow row = new PlaybackControlsRow(mContext);
+        mGlue.setControlsRow(row);
+        PlaybackGlueHostImpl host = new PlaybackGlueHostImpl();
+
+        mGlue.setHost(host);
+        assertSame(mGlue, host.mGlue);
+        assertSame(host, mGlue.getHost());
+        assertTrue(host.mPlaybackRowPresenter instanceof PlaybackTransportRowPresenter);
+        assertSame(host.mRow, row);
+
+    }
+
+    @Test
+    public void customRowAndPresenter() {
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mGlue = new PlaybackTransportControlGlueImpl(mContext);
+            }
+        });
+        PlaybackControlsRow row = new PlaybackControlsRow(mContext);
+        mGlue.setControlsRow(row);
+        PlaybackRowPresenter presenter = new PlaybackRowPresenter() {
+            @Override
+            protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) {
+                return new RowPresenter.ViewHolder(new LinearLayout(parent.getContext()));
+            }
+        };
+        mGlue.setPlaybackRowPresenter(presenter);
+        PlaybackGlueHostImpl host = new PlaybackGlueHostImpl();
+
+        mGlue.setHost(host);
+        assertSame(mGlue, host.mGlue);
+        assertSame(host, mGlue.getHost());
+        assertSame(host.mPlaybackRowPresenter, presenter);
+        assertSame(host.mRow, row);
+
+    }
+
+    @Test
+    public void playerAdapterTest() {
+        mContext = new ContextThemeWrapper(
+                InstrumentationRegistry.getInstrumentation().getTargetContext(),
+                android.support.v17.leanback.test.R.style.Theme_Leanback);
+
+        final PlayerAdapter impl = Mockito.mock(PlayerAdapter.class);
+        when(impl.isPrepared()).thenReturn(true);
+        when(impl.getCurrentPosition()).thenReturn(123L);
+        when(impl.getDuration()).thenReturn(20000L);
+        when(impl.getBufferedPosition()).thenReturn(321L);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mGlue = new PlaybackTransportControlGlueImpl(mContext, impl);
+                PlaybackGlueHostImpl host = new PlaybackGlueHostImpl();
+                mGlue.setHost(host);
+
+                PlaybackTransportRowPresenter presenter = (PlaybackTransportRowPresenter)
+                        mGlue.getPlaybackRowPresenter();
+                FrameLayout parent = new FrameLayout(mContext);
+                mViewHolder = (PlaybackTransportRowPresenter.ViewHolder)
+                        presenter.onCreateViewHolder(parent);
+                presenter.onBindViewHolder(mViewHolder, mGlue.getControlsRow());
+            }
+        });
+
+
+        mGlue.play();
+        Mockito.verify(impl, times(1)).play();
+        mGlue.pause();
+        Mockito.verify(impl, times(1)).pause();
+        mGlue.seekTo(123L);
+        Mockito.verify(impl, times(1)).seekTo(123L);
+        assertEquals(123L, mGlue.getCurrentPosition());
+        assertEquals(20000L, mGlue.getDuration());
+        assertEquals(321L, mGlue.getBufferedPosition());
+
+        assertSame(mGlue.mAdapterCallback, impl.getCallback());
+
+        when(impl.getCurrentPosition()).thenReturn(124L);
+        impl.getCallback().onCurrentPositionChanged(impl);
+        assertEquals(124L, mGlue.getControlsRow().getCurrentPosition());
+
+        when(impl.getBufferedPosition()).thenReturn(333L);
+        impl.getCallback().onBufferedPositionChanged(impl);
+        assertEquals(333L, mGlue.getControlsRow().getBufferedPosition());
+
+        when(impl.getDuration()).thenReturn((long) (Integer.MAX_VALUE) * 2);
+        impl.getCallback().onDurationChanged(impl);
+        assertEquals((long) (Integer.MAX_VALUE) * 2, mGlue.getControlsRow().getDuration());
+
+    }
+
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackGlueHostImplWithViewHolder.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackGlueHostImplWithViewHolder.java
new file mode 100644
index 0000000..d6a1f86
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackGlueHostImplWithViewHolder.java
@@ -0,0 +1,125 @@
+/*
+ * 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.content.Context;
+import android.support.v17.leanback.media.PlaybackGlueHostImpl;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+/**
+ * Example to create a ViewHolder and rebind when notifyPlaybackRowChanged.
+ */
+public class PlaybackGlueHostImplWithViewHolder extends PlaybackGlueHostImpl
+        implements PlaybackSeekUi {
+    protected Context mContext;
+    protected PlaybackRowPresenter.ViewHolder mViewHolder;
+    protected ViewGroup mRootView;
+
+    protected int mLayoutWidth = 1920;
+    protected int mLayoutHeight = ViewGroup.LayoutParams.WRAP_CONTENT;
+    Client mSeekClient;
+
+    public PlaybackGlueHostImplWithViewHolder(Context context) {
+        mContext = context;
+    }
+
+    @Override
+    public void setPlaybackRow(Row row) {
+        super.setPlaybackRow(row);
+        createViewHolderIfNeeded();
+    }
+
+    @Override
+    public void setPlaybackRowPresenter(PlaybackRowPresenter presenter) {
+        super.setPlaybackRowPresenter(presenter);
+        createViewHolderIfNeeded();
+    }
+
+    void createViewHolderIfNeeded() {
+        if (mViewHolder == null && mPlaybackRowPresenter != null && mRow != null) {
+            mViewHolder = (PlaybackRowPresenter.ViewHolder)
+                    mPlaybackRowPresenter.onCreateViewHolder(mRootView = new FrameLayout(mContext));
+            mRootView.addView(mViewHolder.view, mLayoutWidth, mLayoutHeight);
+            mRootView.measure(
+                    View.MeasureSpec.makeMeasureSpec(1920, View.MeasureSpec.AT_MOST),
+                    View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
+            mRootView.layout(0, 0, mRootView.getMeasuredWidth(), mRootView.getMeasuredHeight());
+            mPlaybackRowPresenter.onBindViewHolder(mViewHolder, mRow);
+            if (mViewHolder instanceof PlaybackSeekUi) {
+                ((PlaybackSeekUi) mViewHolder).setPlaybackSeekUiClient(mChainedClient);
+            }
+        }
+    }
+
+    @Override
+    public void notifyPlaybackRowChanged() {
+        if (mViewHolder != null) {
+            mPlaybackRowPresenter.onUnbindRowViewHolder(mViewHolder);
+            mPlaybackRowPresenter.onBindViewHolder(mViewHolder, mRow);
+        }
+    }
+
+    public void sendKeyEvent(KeyEvent event) {
+        mRootView.dispatchKeyEvent(event);
+    }
+
+    public void sendKeyDownUp(int keyCode) {
+        sendKeyDownUp(keyCode, 1);
+    }
+
+    public void sendKeyDownUp(int keyCode, int repeat) {
+        for (int i = 0; i < repeat; i++) {
+            mRootView.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
+        }
+        mRootView.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
+    }
+
+    @Override
+    public void setPlaybackSeekUiClient(Client client) {
+        mSeekClient = client;
+    }
+
+    protected Client mChainedClient = new Client() {
+        @Override
+        public boolean isSeekEnabled() {
+            return mSeekClient == null ? false : mSeekClient.isSeekEnabled();
+        }
+
+        @Override
+        public void onSeekStarted() {
+            mSeekClient.onSeekStarted();
+        }
+
+        @Override
+        public PlaybackSeekDataProvider getPlaybackSeekDataProvider() {
+            return mSeekClient.getPlaybackSeekDataProvider();
+        }
+
+        @Override
+        public void onSeekPositionChanged(long pos) {
+            mSeekClient.onSeekPositionChanged(pos);
+        }
+
+        @Override
+        public void onSeekFinished(boolean cancelled) {
+            mSeekClient.onSeekFinished(cancelled);
+        }
+    };
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackSeekProviderSample.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackSeekProviderSample.java
new file mode 100644
index 0000000..6232e01
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackSeekProviderSample.java
@@ -0,0 +1,38 @@
+/*
+ * 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;
+
+public class PlaybackSeekProviderSample extends PlaybackSeekDataProvider {
+
+    protected long[] mSeekPositions;
+
+    public PlaybackSeekProviderSample(long duration, int numSeekPositions) {
+        this(0, duration, numSeekPositions);
+    }
+
+    public PlaybackSeekProviderSample(long first, long last, int numSeekPositions) {
+        mSeekPositions = new long[numSeekPositions];
+        for (int i = 0; i < mSeekPositions.length; i++) {
+            mSeekPositions[i] = first + i * (last - first) / (numSeekPositions - 1);
+        }
+    }
+
+    @Override
+    public long[] getSeekPositions() {
+        return mSeekPositions;
+    }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackTransportRowPresenterTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackTransportRowPresenterTest.java
new file mode 100644
index 0000000..54d27a3
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackTransportRowPresenterTest.java
@@ -0,0 +1,527 @@
+/*
+ * 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 junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.ColorDrawable;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.v17.leanback.media.PlaybackTransportControlGlue;
+import android.support.v17.leanback.media.PlayerAdapter;
+import android.support.v17.leanback.widget.PlaybackSeekDataProvider.ResultCallback;
+import android.view.ContextThemeWrapper;
+import android.view.KeyEvent;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.util.Arrays;
+
+@SmallTest
+public class PlaybackTransportRowPresenterTest {
+
+    Context mContext;
+    PlaybackTransportControlGlue mGlue;
+    PlaybackGlueHostImplWithViewHolder mHost;
+    PlayerAdapter mImpl;
+    PlaybackTransportRowPresenter.ViewHolder mViewHolder;
+    AbstractDetailsDescriptionPresenter.ViewHolder mDescriptionViewHolder;
+    int mNumbThumbs;
+
+    @Before
+    public void setUp() {
+        mContext = new ContextThemeWrapper(
+                InstrumentationRegistry.getInstrumentation().getTargetContext(),
+                android.support.v17.leanback.test.R.style.Theme_Leanback);
+        mHost = new PlaybackGlueHostImplWithViewHolder(mContext);
+        mImpl = Mockito.mock(PlayerAdapter.class);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mGlue = new PlaybackTransportControlGlue(mContext, mImpl);
+                mGlue.setHost(mHost);
+
+            }
+        });
+        mViewHolder = (PlaybackTransportRowPresenter.ViewHolder) mHost.mViewHolder;
+        mDescriptionViewHolder = (AbstractDetailsDescriptionPresenter.ViewHolder)
+                mViewHolder.mDescriptionViewHolder;
+        mNumbThumbs = mViewHolder.mThumbsBar.getChildCount();
+        assertTrue((mNumbThumbs & 1) != 0);
+    }
+
+    void sendKeyUIThread(int keyCode) {
+        sendKeyUIThread(keyCode, 1);
+    }
+
+    void sendKeyUIThread(final int keyCode, final int repeat) {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mHost.sendKeyDownUp(keyCode, repeat);
+            }
+        });
+    }
+
+    void verifyGetThumbCalls(int firstHeroIndex, int lastHeroIndex,
+            PlaybackSeekDataProvider provider, long[] positions) {
+        int firstThumbIndex = Math.max(firstHeroIndex - (mNumbThumbs / 2), 0);
+        int lastThumbIndex = Math.min(lastHeroIndex + (mNumbThumbs / 2), positions.length - 1);
+        for (int i = firstThumbIndex; i <= lastThumbIndex; i++) {
+            Mockito.verify(provider, times(1)).getThumbnail(eq(i), any(ResultCallback.class));
+        }
+        Mockito.verify(provider, times(0)).getThumbnail(
+                eq(firstThumbIndex - 1), any(ResultCallback.class));
+        Mockito.verify(provider, times(0)).getThumbnail(
+                eq(firstThumbIndex - 2), any(ResultCallback.class));
+        Mockito.verify(provider, times(0)).getThumbnail(
+                eq(lastThumbIndex + 1), any(ResultCallback.class));
+        Mockito.verify(provider, times(0)).getThumbnail(
+                eq(lastThumbIndex + 2), any(ResultCallback.class));
+    }
+
+    void verifyAtHeroIndexWithDifferentPosition(long position, int heroIndex) {
+        assertEquals(position, mGlue.getControlsRow().getCurrentPosition());
+        assertEquals(mViewHolder.mThumbHeroIndex, heroIndex);
+    }
+
+    void verifyAtHeroIndex(long[] positions, int heroIndex) {
+        verifyAtHeroIndex(positions, heroIndex, null);
+    }
+
+    void verifyAtHeroIndex(long[] positions, int heroIndex, Bitmap[] thumbs) {
+        assertEquals(positions[heroIndex], mGlue.getControlsRow().getCurrentPosition());
+        assertEquals(mViewHolder.mThumbHeroIndex, heroIndex);
+        if (thumbs != null) {
+            int start = Math.max(0, mViewHolder.mThumbHeroIndex - mNumbThumbs / 2);
+            int end = Math.min(positions.length - 1, mViewHolder.mThumbHeroIndex + mNumbThumbs / 2);
+            verifyThumbBitmaps(thumbs, start, end,
+                    mViewHolder.mThumbsBar, start + mNumbThumbs / 2 - mViewHolder.mThumbHeroIndex,
+                    end + mNumbThumbs / 2 - mViewHolder.mThumbHeroIndex);
+        }
+    }
+
+    void verifyThumbBitmaps(Bitmap[] thumbs, int start, int end,
+            ThumbsBar thumbsBar, int childStart, int childEnd) {
+        assertEquals(end - start, childEnd - childStart);
+        for (int i = start; i <= end; i++) {
+            assertSame(thumbs[i], thumbsBar.getThumbBitmap(childStart + (i - start)));
+        }
+        for (int i = 0; i < childStart; i++) {
+            assertNull(thumbsBar.getThumbBitmap(i));
+        }
+        for (int i = childEnd + 1; i < mNumbThumbs; i++) {
+            assertNull(thumbsBar.getThumbBitmap(i));
+        }
+    }
+
+    @Test
+    public void progressUpdating() {
+        when(mImpl.isPrepared()).thenReturn(true);
+        when(mImpl.getCurrentPosition()).thenReturn(123L);
+        when(mImpl.getDuration()).thenReturn(20000L);
+        when(mImpl.getBufferedPosition()).thenReturn(321L);
+
+        mGlue.play();
+        Mockito.verify(mImpl, times(1)).play();
+        mGlue.pause();
+        Mockito.verify(mImpl, times(1)).pause();
+        mGlue.seekTo(1231);
+        Mockito.verify(mImpl, times(1)).seekTo(1231);
+        mImpl.getCallback().onCurrentPositionChanged(mImpl);
+        mImpl.getCallback().onDurationChanged(mImpl);
+        mImpl.getCallback().onBufferedPositionChanged(mImpl);
+        assertEquals(123L, mGlue.getCurrentPosition());
+        assertEquals(20000L, mGlue.getDuration());
+        assertEquals(321L, mGlue.getBufferedPosition());
+        assertEquals(123L, mViewHolder.mCurrentTimeInMs);
+        assertEquals(20000L, mViewHolder.mTotalTimeInMs);
+        assertEquals(321L, mViewHolder.mSecondaryProgressInMs);
+
+        when(mImpl.getCurrentPosition()).thenReturn(124L);
+        mImpl.getCallback().onCurrentPositionChanged(mImpl);
+        assertEquals(124L, mGlue.getControlsRow().getCurrentPosition());
+        assertEquals(124L, mViewHolder.mCurrentTimeInMs);
+        when(mImpl.getBufferedPosition()).thenReturn(333L);
+        mImpl.getCallback().onBufferedPositionChanged(mImpl);
+        assertEquals(333L, mGlue.getControlsRow().getBufferedPosition());
+        assertEquals(333L, mViewHolder.mSecondaryProgressInMs);
+        when(mImpl.getDuration()).thenReturn((long) (Integer.MAX_VALUE) * 2);
+        mImpl.getCallback().onDurationChanged(mImpl);
+        assertEquals((long) (Integer.MAX_VALUE) * 2, mGlue.getControlsRow().getDuration());
+        assertEquals((long) (Integer.MAX_VALUE) * 2, mViewHolder.mTotalTimeInMs);
+    }
+
+    @Test
+    public void mediaInfo() {
+        final ColorDrawable art = new ColorDrawable();
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mGlue.setTitle("xyz");
+                mGlue.setSubtitle("zyx");
+                mGlue.setArt(art);
+            }
+        });
+        assertEquals("xyz", mDescriptionViewHolder.mTitle.getText());
+        assertEquals("zyx", mDescriptionViewHolder.mSubtitle.getText());
+        assertSame(art, mViewHolder.mImageView.getDrawable());
+    }
+
+    @Test
+    public void seekAndConfirm() {
+        when(mImpl.isPrepared()).thenReturn(true);
+        when(mImpl.getCurrentPosition()).thenReturn(0L);
+        when(mImpl.getDuration()).thenReturn(20000L);
+        when(mImpl.getBufferedPosition()).thenReturn(321L);
+        mImpl.getCallback().onCurrentPositionChanged(mImpl);
+        mImpl.getCallback().onDurationChanged(mImpl);
+        mImpl.getCallback().onBufferedPositionChanged(mImpl);
+
+        PlaybackSeekProviderSample provider = Mockito.spy(
+                new PlaybackSeekProviderSample(10000L, 101));
+        final long[] positions = provider.getSeekPositions();
+        mGlue.setSeekProvider(provider);
+        mViewHolder.mProgressBar.requestFocus();
+        assertTrue(mViewHolder.mProgressBar.hasFocus());
+
+        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
+        verifyAtHeroIndex(positions, 1);
+        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
+        verifyAtHeroIndex(positions, 2);
+
+        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_CENTER);
+        Mockito.verify(mImpl).seekTo(positions[2]);
+
+        verifyGetThumbCalls(1, 2, provider, positions);
+    }
+
+
+    @Test
+    public void seekHoldKeyDown() {
+        when(mImpl.isPrepared()).thenReturn(true);
+        when(mImpl.getCurrentPosition()).thenReturn(4489L);
+        when(mImpl.getDuration()).thenReturn(20000L);
+        when(mImpl.getBufferedPosition()).thenReturn(4489L);
+        mImpl.getCallback().onCurrentPositionChanged(mImpl);
+        mImpl.getCallback().onDurationChanged(mImpl);
+        mImpl.getCallback().onBufferedPositionChanged(mImpl);
+
+        PlaybackSeekProviderSample provider = Mockito.spy(
+                new PlaybackSeekProviderSample(10000L, 101));
+        final long[] positions = provider.getSeekPositions();
+        mGlue.setSeekProvider(provider);
+        mViewHolder.mProgressBar.requestFocus();
+        assertTrue(mViewHolder.mProgressBar.hasFocus());
+
+        int insertPosition = -1 - Arrays.binarySearch(positions, 4489L);
+        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT, 5);
+        verifyAtHeroIndex(positions, insertPosition + 4);
+        verifyGetThumbCalls(insertPosition, insertPosition + 4, provider, positions);
+
+        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_LEFT, 5);
+        verifyAtHeroIndex(positions, insertPosition - 1);
+    }
+
+    @Test
+    public void seekAndCancel() {
+        when(mImpl.isPrepared()).thenReturn(true);
+        when(mImpl.getCurrentPosition()).thenReturn(0L);
+        when(mImpl.getDuration()).thenReturn(20000L);
+        when(mImpl.getBufferedPosition()).thenReturn(321L);
+        mImpl.getCallback().onCurrentPositionChanged(mImpl);
+        mImpl.getCallback().onDurationChanged(mImpl);
+        mImpl.getCallback().onBufferedPositionChanged(mImpl);
+
+        PlaybackSeekProviderSample provider = Mockito.spy(
+                new PlaybackSeekProviderSample(10000L, 101));
+        final long[] positions = provider.getSeekPositions();
+        mGlue.setSeekProvider(provider);
+        mViewHolder.mProgressBar.requestFocus();
+        assertTrue(mViewHolder.mProgressBar.hasFocus());
+
+        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
+        verifyAtHeroIndex(positions, 1);
+
+        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
+        verifyAtHeroIndex(positions, 2);
+
+        sendKeyUIThread(KeyEvent.KEYCODE_BACK);
+        Mockito.verify(mImpl, times(0)).seekTo(anyInt());
+        verifyGetThumbCalls(1, 2, provider, positions);
+    }
+
+    @Test
+    public void seekUpBetweenTwoKeyPosition() {
+        PlaybackSeekProviderSample provider = Mockito.spy(
+                new PlaybackSeekProviderSample(10000L, 101));
+        final long[] positions = provider.getSeekPositions();
+
+        // initially select between 0 and 1
+        when(mImpl.isPrepared()).thenReturn(true);
+        when(mImpl.getCurrentPosition()).thenReturn((positions[0] + positions[1]) / 2);
+        mImpl.getCallback().onCurrentPositionChanged(mImpl);
+        when(mImpl.getDuration()).thenReturn(20000L);
+        when(mImpl.getBufferedPosition()).thenReturn(321L);
+        mImpl.getCallback().onDurationChanged(mImpl);
+        mImpl.getCallback().onBufferedPositionChanged(mImpl);
+
+        mGlue.setSeekProvider(provider);
+        mViewHolder.mProgressBar.requestFocus();
+        assertTrue(mViewHolder.mProgressBar.hasFocus());
+
+        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
+        verifyAtHeroIndex(positions, 1);
+        verifyGetThumbCalls(1, 1, provider, positions);
+    }
+
+    @Test
+    public void seekDownBetweenTwoKeyPosition() {
+        PlaybackSeekProviderSample provider = Mockito.spy(
+                new PlaybackSeekProviderSample(10000L, 101));
+        final long[] positions = provider.getSeekPositions();
+        assertTrue(positions[0] == 0);
+
+        // initially select between 0 and 1
+        when(mImpl.isPrepared()).thenReturn(true);
+        when(mImpl.getCurrentPosition()).thenReturn((positions[0] + positions[1]) / 2);
+        mImpl.getCallback().onCurrentPositionChanged(mImpl);
+        when(mImpl.getDuration()).thenReturn(20000L);
+        when(mImpl.getBufferedPosition()).thenReturn(321L);
+        mImpl.getCallback().onDurationChanged(mImpl);
+        mImpl.getCallback().onBufferedPositionChanged(mImpl);
+
+        mGlue.setSeekProvider(provider);
+        mViewHolder.mProgressBar.requestFocus();
+        assertTrue(mViewHolder.mProgressBar.hasFocus());
+
+        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_LEFT);
+        verifyAtHeroIndex(positions, 0);
+        verifyGetThumbCalls(0, 0, provider, positions);
+    }
+
+    @Test
+    public void seekDownOutOfKeyPositions() {
+        PlaybackSeekProviderSample provider = Mockito.spy(
+                new PlaybackSeekProviderSample(1000L, 10000L, 101));
+        final long[] positions = provider.getSeekPositions();
+        assertTrue(positions[0] > 0);
+
+        // initially select between 0 and 1
+        when(mImpl.isPrepared()).thenReturn(true);
+        when(mImpl.getCurrentPosition()).thenReturn((positions[0] + positions[1]) / 2);
+        mImpl.getCallback().onCurrentPositionChanged(mImpl);
+        when(mImpl.getDuration()).thenReturn(20000L);
+        when(mImpl.getBufferedPosition()).thenReturn(321L);
+        mImpl.getCallback().onDurationChanged(mImpl);
+        mImpl.getCallback().onBufferedPositionChanged(mImpl);
+
+        mGlue.setSeekProvider(provider);
+        mViewHolder.mProgressBar.requestFocus();
+        assertTrue(mViewHolder.mProgressBar.hasFocus());
+
+        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_LEFT);
+        verifyAtHeroIndex(positions, 0);
+        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_LEFT);
+        verifyAtHeroIndexWithDifferentPosition(0, 0);
+        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_LEFT);
+        verifyAtHeroIndexWithDifferentPosition(0, 0);
+        verifyGetThumbCalls(0, 0, provider, positions);
+    }
+
+    @Test
+    public void seekDownAheadOfKeyPositions() {
+        PlaybackSeekProviderSample provider = Mockito.spy(
+                new PlaybackSeekProviderSample(1000L, 10000L, 101));
+        final long[] positions = provider.getSeekPositions();
+        assertTrue(positions[0] > 0);
+
+        // initially select between 0 and 1
+        when(mImpl.isPrepared()).thenReturn(true);
+        when(mImpl.getCurrentPosition()).thenReturn(positions[0] / 2);
+        mImpl.getCallback().onCurrentPositionChanged(mImpl);
+        when(mImpl.getDuration()).thenReturn(20000L);
+        when(mImpl.getBufferedPosition()).thenReturn(321L);
+        mImpl.getCallback().onDurationChanged(mImpl);
+        mImpl.getCallback().onBufferedPositionChanged(mImpl);
+
+        mGlue.setSeekProvider(provider);
+        mViewHolder.mProgressBar.requestFocus();
+        assertTrue(mViewHolder.mProgressBar.hasFocus());
+
+        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_LEFT);
+        verifyAtHeroIndexWithDifferentPosition(0, 0);
+        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
+        verifyAtHeroIndex(positions, 0);
+        verifyGetThumbCalls(0, 0, provider, positions);
+    }
+
+    @Test
+    public void seekUpAheadOfKeyPositions() {
+        PlaybackSeekProviderSample provider = Mockito.spy(
+                new PlaybackSeekProviderSample(1000L, 10000L, 101));
+        final long[] positions = provider.getSeekPositions();
+        assertTrue(positions[0] > 0);
+
+        // initially select between 0 and 1
+        when(mImpl.isPrepared()).thenReturn(true);
+        when(mImpl.getCurrentPosition()).thenReturn(positions[0] / 2);
+        mImpl.getCallback().onCurrentPositionChanged(mImpl);
+        when(mImpl.getDuration()).thenReturn(20000L);
+        when(mImpl.getBufferedPosition()).thenReturn(321L);
+        mImpl.getCallback().onDurationChanged(mImpl);
+        mImpl.getCallback().onBufferedPositionChanged(mImpl);
+
+        mGlue.setSeekProvider(provider);
+        mViewHolder.mProgressBar.requestFocus();
+        assertTrue(mViewHolder.mProgressBar.hasFocus());
+
+        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
+        verifyAtHeroIndex(positions, 0);
+        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_LEFT);
+        verifyAtHeroIndexWithDifferentPosition(0, 0);
+        verifyGetThumbCalls(0, 0, provider, positions);
+    }
+
+    @Test
+    public void seekUpOutOfKeyPositions() {
+        PlaybackSeekProviderSample provider = Mockito.spy(
+                new PlaybackSeekProviderSample(10000L, 101));
+        final long[] positions = provider.getSeekPositions();
+
+        // initially select between nth-1 and nth
+        when(mImpl.isPrepared()).thenReturn(true);
+        when(mImpl.getCurrentPosition()).thenReturn((positions[positions.length - 2]
+                + positions[positions.length - 1]) / 2);
+        mImpl.getCallback().onCurrentPositionChanged(mImpl);
+        when(mImpl.getDuration()).thenReturn(20000L);
+        when(mImpl.getBufferedPosition()).thenReturn(321L);
+        mImpl.getCallback().onDurationChanged(mImpl);
+        mImpl.getCallback().onBufferedPositionChanged(mImpl);
+
+        mGlue.setSeekProvider(provider);
+        mViewHolder.mProgressBar.requestFocus();
+        assertTrue(mViewHolder.mProgressBar.hasFocus());
+
+        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
+        verifyAtHeroIndex(positions, positions.length - 1);
+        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_LEFT);
+        verifyAtHeroIndex(positions, positions.length - 2);
+        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
+        verifyAtHeroIndex(positions, positions.length - 1);
+        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
+        verifyAtHeroIndexWithDifferentPosition(20000L, positions.length - 1);
+        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
+        verifyAtHeroIndexWithDifferentPosition(20000L, positions.length - 1);
+        verifyGetThumbCalls(positions.length - 2, positions.length - 1, provider, positions);
+    }
+
+    @Test
+    public void seekUpAfterKeyPositions() {
+        PlaybackSeekProviderSample provider = Mockito.spy(
+                new PlaybackSeekProviderSample(10000L, 101));
+        final long[] positions = provider.getSeekPositions();
+
+        // initially select after last item
+        when(mImpl.isPrepared()).thenReturn(true);
+        when(mImpl.getCurrentPosition()).thenReturn(positions[positions.length - 1] + 100);
+        mImpl.getCallback().onCurrentPositionChanged(mImpl);
+        when(mImpl.getDuration()).thenReturn(20000L);
+        when(mImpl.getBufferedPosition()).thenReturn(321L);
+        mImpl.getCallback().onDurationChanged(mImpl);
+        mImpl.getCallback().onBufferedPositionChanged(mImpl);
+
+        mGlue.setSeekProvider(provider);
+        mViewHolder.mProgressBar.requestFocus();
+        assertTrue(mViewHolder.mProgressBar.hasFocus());
+
+        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
+        verifyAtHeroIndexWithDifferentPosition(20000L, positions.length - 1);
+        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_LEFT);
+        verifyAtHeroIndex(positions, positions.length - 1);
+        verifyGetThumbCalls(positions.length - 1, positions.length - 1, provider, positions);
+    }
+
+    @Test
+    public void seekDownAfterKeyPositions() {
+        PlaybackSeekProviderSample provider = Mockito.spy(
+                new PlaybackSeekProviderSample(10000L, 101));
+        final long[] positions = provider.getSeekPositions();
+
+        // initially select after last item
+        when(mImpl.isPrepared()).thenReturn(true);
+        when(mImpl.getCurrentPosition()).thenReturn(positions[positions.length - 1] + 100);
+        mImpl.getCallback().onCurrentPositionChanged(mImpl);
+        when(mImpl.getDuration()).thenReturn(20000L);
+        when(mImpl.getBufferedPosition()).thenReturn(321L);
+        mImpl.getCallback().onDurationChanged(mImpl);
+        mImpl.getCallback().onBufferedPositionChanged(mImpl);
+
+        mGlue.setSeekProvider(provider);
+        mViewHolder.mProgressBar.requestFocus();
+        assertTrue(mViewHolder.mProgressBar.hasFocus());
+
+        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_LEFT);
+        verifyAtHeroIndex(positions, positions.length - 1);
+        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
+        verifyAtHeroIndexWithDifferentPosition(20000L, positions.length - 1);
+        verifyGetThumbCalls(positions.length - 1, positions.length - 1, provider, positions);
+    }
+
+    @Test
+    public void thumbLoadedInCallback() {
+        when(mImpl.isPrepared()).thenReturn(true);
+        when(mImpl.getCurrentPosition()).thenReturn(0L);
+        when(mImpl.getDuration()).thenReturn(20000L);
+        when(mImpl.getBufferedPosition()).thenReturn(321L);
+        mImpl.getCallback().onCurrentPositionChanged(mImpl);
+        mImpl.getCallback().onDurationChanged(mImpl);
+        mImpl.getCallback().onBufferedPositionChanged(mImpl);
+
+        final Bitmap[] thumbs = new Bitmap[101];
+        for (int i = 0; i < 101; i++) {
+            thumbs[i] = Bitmap.createBitmap(16, 16, Bitmap.Config.ARGB_8888);
+        }
+        PlaybackSeekProviderSample provider = new PlaybackSeekProviderSample(10000L, 101) {
+            @Override
+            public void getThumbnail(int index, ResultCallback callback) {
+                callback.onThumbnailLoaded(thumbs[index], index);
+            }
+        };
+        final long[] positions = provider.getSeekPositions();
+        mGlue.setSeekProvider(provider);
+        mViewHolder.mProgressBar.requestFocus();
+        assertTrue(mViewHolder.mProgressBar.hasFocus());
+
+        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
+        verifyAtHeroIndex(positions, 1, thumbs);
+    }
+
+}