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 {
+ * @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() {
+ * @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);
+ }
+
+}