/* * 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 androidx.leanback.media; import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; import android.content.Context; import android.util.Log; import android.view.KeyEvent; import android.view.View; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.RestrictTo; import androidx.leanback.widget.AbstractDetailsDescriptionPresenter; import androidx.leanback.widget.Action; import androidx.leanback.widget.ArrayObjectAdapter; import androidx.leanback.widget.ObjectAdapter; import androidx.leanback.widget.PlaybackControlsRow; import androidx.leanback.widget.PlaybackControlsRowPresenter; import androidx.leanback.widget.PlaybackRowPresenter; import androidx.leanback.widget.RowPresenter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * 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 that manages interaction between the * leanback UI components {@link PlaybackControlsRow} {@link PlaybackControlsRowPresenter} * and a functional {@link PlayerAdapter} which represents the underlying * media player. * *

Apps must pass a {@link PlayerAdapter} in the constructor for a specific * implementation e.g. a {@link MediaPlayerAdapter}. *

* *

The glue has two action bars: primary action bars and secondary action bars. Apps * can provide additional actions by overriding {@link #onCreatePrimaryActions} and / or * {@link #onCreateSecondaryActions} and respond to actions by overriding * {@link #onActionClicked(Action)}. *

* *

The subclass is responsible for implementing the "repeat mode" in * {@link #onPlayCompleted()}. *

* * Sample Code: *

 * public class MyVideoFragment extends VideoFragment {
 *     @Override
 *     public void onCreate(Bundle savedInstanceState) {
 *         super.onCreate(savedInstanceState);
 *         PlaybackBannerControlGlue playerGlue =
 *                 new PlaybackBannerControlGlue(getActivity(),
 *                         new MediaPlayerAdapter(getActivity()));
 *         playerGlue.setHost(new VideoFragmentGlueHost(this));
 *         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));
 *         playerGlue.playWhenPrepared();
 *     }
 * }
 * 
* @param Type of {@link PlayerAdapter} passed in constructor. */ public class PlaybackBannerControlGlue extends PlaybackBaseControlGlue { /** @hide */ @IntDef( flag = true, value = { ACTION_CUSTOM_LEFT_FIRST, ACTION_SKIP_TO_PREVIOUS, ACTION_REWIND, ACTION_PLAY_PAUSE, ACTION_FAST_FORWARD, ACTION_SKIP_TO_NEXT, ACTION_CUSTOM_RIGHT_FIRST }) @RestrictTo(LIBRARY_GROUP) @Retention(RetentionPolicy.SOURCE) public @interface ACTION_ {} /** * The adapter key for the first custom control on the left side * of the predefined primary controls. */ public static final int ACTION_CUSTOM_LEFT_FIRST = PlaybackBaseControlGlue.ACTION_CUSTOM_LEFT_FIRST; /** * The adapter key for the skip to previous control. */ public static final int ACTION_SKIP_TO_PREVIOUS = PlaybackBaseControlGlue.ACTION_SKIP_TO_PREVIOUS; /** * The adapter key for the rewind control. */ public static final int ACTION_REWIND = PlaybackBaseControlGlue.ACTION_REWIND; /** * The adapter key for the play/pause control. */ public static final int ACTION_PLAY_PAUSE = PlaybackBaseControlGlue.ACTION_PLAY_PAUSE; /** * The adapter key for the fast forward control. */ public static final int ACTION_FAST_FORWARD = PlaybackBaseControlGlue.ACTION_FAST_FORWARD; /** * The adapter key for the skip to next control. */ public static final int ACTION_SKIP_TO_NEXT = PlaybackBaseControlGlue.ACTION_SKIP_TO_NEXT; /** * The adapter key for the first custom control on the right side * of the predefined primary controls. */ public static final int ACTION_CUSTOM_RIGHT_FIRST = PlaybackBaseControlGlue.ACTION_CUSTOM_RIGHT_FIRST; /** @hide */ @IntDef({ PLAYBACK_SPEED_INVALID, PLAYBACK_SPEED_PAUSED, PLAYBACK_SPEED_NORMAL, PLAYBACK_SPEED_FAST_L0, PLAYBACK_SPEED_FAST_L1, PLAYBACK_SPEED_FAST_L2, PLAYBACK_SPEED_FAST_L3, PLAYBACK_SPEED_FAST_L4 }) @RestrictTo(LIBRARY_GROUP) @Retention(RetentionPolicy.SOURCE) private @interface SPEED {} /** * Invalid playback speed. */ public static final int PLAYBACK_SPEED_INVALID = -1; /** * Speed representing playback state that is paused. */ public static final int PLAYBACK_SPEED_PAUSED = 0; /** * Speed representing playback state that is playing normally. */ public static final int PLAYBACK_SPEED_NORMAL = 1; /** * The initial (level 0) fast forward playback speed. * The negative of this value is for rewind at the same speed. */ public static final int PLAYBACK_SPEED_FAST_L0 = 10; /** * The level 1 fast forward playback speed. * The negative of this value is for rewind at the same speed. */ public static final int PLAYBACK_SPEED_FAST_L1 = 11; /** * The level 2 fast forward playback speed. * The negative of this value is for rewind at the same speed. */ public static final int PLAYBACK_SPEED_FAST_L2 = 12; /** * The level 3 fast forward playback speed. * The negative of this value is for rewind at the same speed. */ public static final int PLAYBACK_SPEED_FAST_L3 = 13; /** * The level 4 fast forward playback speed. * The negative of this value is for rewind at the same speed. */ public static final int PLAYBACK_SPEED_FAST_L4 = 14; private static final String TAG = PlaybackBannerControlGlue.class.getSimpleName(); private static final int NUMBER_OF_SEEK_SPEEDS = PLAYBACK_SPEED_FAST_L4 - PLAYBACK_SPEED_FAST_L0 + 1; private final int[] mFastForwardSpeeds; private final int[] mRewindSpeeds; private PlaybackControlsRow.SkipNextAction mSkipNextAction; private PlaybackControlsRow.SkipPreviousAction mSkipPreviousAction; private PlaybackControlsRow.FastForwardAction mFastForwardAction; private PlaybackControlsRow.RewindAction mRewindAction; @SPEED private int mPlaybackSpeed = PLAYBACK_SPEED_PAUSED; private long mStartTime; private long mStartPosition = 0; // Flag for is customized FastForward/ Rewind Action supported. // If customized actions are not supported, the adapter can still use default behavior through // setting ACTION_REWIND and ACTION_FAST_FORWARD as supported actions. private boolean mIsCustomizedFastForwardSupported; private boolean mIsCustomizedRewindSupported; /** * Constructor for the glue. * * @param context * @param seekSpeeds The array of seek speeds for fast forward and rewind. The maximum length of * the array is defined as NUMBER_OF_SEEK_SPEEDS. * @param impl Implementation to underlying media player. */ public PlaybackBannerControlGlue(Context context, int[] seekSpeeds, T impl) { this(context, seekSpeeds, seekSpeeds, impl); } /** * Constructor for the glue. * * @param context * @param fastForwardSpeeds The array of seek speeds for fast forward. The maximum length of * the array is defined as NUMBER_OF_SEEK_SPEEDS. * @param rewindSpeeds The array of seek speeds for rewind. The maximum length of * the array is defined as NUMBER_OF_SEEK_SPEEDS. * @param impl Implementation to underlying media player. */ public PlaybackBannerControlGlue(Context context, int[] fastForwardSpeeds, int[] rewindSpeeds, T impl) { super(context, impl); if (fastForwardSpeeds.length == 0 || fastForwardSpeeds.length > NUMBER_OF_SEEK_SPEEDS) { throw new IllegalArgumentException("invalid fastForwardSpeeds array size"); } mFastForwardSpeeds = fastForwardSpeeds; if (rewindSpeeds.length == 0 || rewindSpeeds.length > NUMBER_OF_SEEK_SPEEDS) { throw new IllegalArgumentException("invalid rewindSpeeds array size"); } mRewindSpeeds = rewindSpeeds; if ((mPlayerAdapter.getSupportedActions() & ACTION_FAST_FORWARD) != 0) { mIsCustomizedFastForwardSupported = true; } if ((mPlayerAdapter.getSupportedActions() & ACTION_REWIND) != 0) { mIsCustomizedRewindSupported = true; } } @Override public void setControlsRow(PlaybackControlsRow controlsRow) { super.setControlsRow(controlsRow); onUpdatePlaybackState(); } @Override protected void onCreatePrimaryActions(ArrayObjectAdapter primaryActionsAdapter) { final long supportedActions = getSupportedActions(); if ((supportedActions & ACTION_SKIP_TO_PREVIOUS) != 0 && mSkipPreviousAction == null) { primaryActionsAdapter.add(mSkipPreviousAction = new PlaybackControlsRow.SkipPreviousAction(getContext())); } else if ((supportedActions & ACTION_SKIP_TO_PREVIOUS) == 0 && mSkipPreviousAction != null) { primaryActionsAdapter.remove(mSkipPreviousAction); mSkipPreviousAction = null; } if ((supportedActions & ACTION_REWIND) != 0 && mRewindAction == null) { primaryActionsAdapter.add(mRewindAction = new PlaybackControlsRow.RewindAction(getContext(), mRewindSpeeds.length)); } else if ((supportedActions & ACTION_REWIND) == 0 && mRewindAction != null) { primaryActionsAdapter.remove(mRewindAction); mRewindAction = null; } if ((supportedActions & ACTION_PLAY_PAUSE) != 0 && mPlayPauseAction == null) { mPlayPauseAction = new PlaybackControlsRow.PlayPauseAction(getContext()); primaryActionsAdapter.add(mPlayPauseAction = new PlaybackControlsRow.PlayPauseAction(getContext())); } else if ((supportedActions & ACTION_PLAY_PAUSE) == 0 && mPlayPauseAction != null) { primaryActionsAdapter.remove(mPlayPauseAction); mPlayPauseAction = null; } if ((supportedActions & ACTION_FAST_FORWARD) != 0 && mFastForwardAction == null) { mFastForwardAction = new PlaybackControlsRow.FastForwardAction(getContext(), mFastForwardSpeeds.length); primaryActionsAdapter.add(mFastForwardAction = new PlaybackControlsRow.FastForwardAction(getContext(), mFastForwardSpeeds.length)); } else if ((supportedActions & ACTION_FAST_FORWARD) == 0 && mFastForwardAction != null) { primaryActionsAdapter.remove(mFastForwardAction); mFastForwardAction = null; } if ((supportedActions & ACTION_SKIP_TO_NEXT) != 0 && mSkipNextAction == null) { primaryActionsAdapter.add(mSkipNextAction = new PlaybackControlsRow.SkipNextAction(getContext())); } else if ((supportedActions & ACTION_SKIP_TO_NEXT) == 0 && mSkipNextAction != null) { primaryActionsAdapter.remove(mSkipNextAction); mSkipNextAction = null; } } @Override protected PlaybackRowPresenter onCreateRowPresenter() { final AbstractDetailsDescriptionPresenter detailsPresenter = new AbstractDetailsDescriptionPresenter() { @Override protected void onBindDescription(ViewHolder viewHolder, Object object) { PlaybackBannerControlGlue glue = (PlaybackBannerControlGlue) object; viewHolder.getTitle().setText(glue.getTitle()); viewHolder.getSubtitle().setText(glue.getSubtitle()); } }; PlaybackControlsRowPresenter rowPresenter = new PlaybackControlsRowPresenter(detailsPresenter) { @Override protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) { super.onBindRowViewHolder(vh, item); vh.setOnKeyListener(PlaybackBannerControlGlue.this); } @Override protected void onUnbindRowViewHolder(RowPresenter.ViewHolder vh) { super.onUnbindRowViewHolder(vh); vh.setOnKeyListener(null); } }; return rowPresenter; } /** * 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: boolean abortSeek = mPlaybackSpeed >= PLAYBACK_SPEED_FAST_L0 || mPlaybackSpeed <= -PLAYBACK_SPEED_FAST_L0; if (abortSeek) { play(); onUpdatePlaybackStatusAfterUserAction(); return keyCode == KeyEvent.KEYCODE_BACK || keyCode == 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; } void onUpdatePlaybackStatusAfterUserAction() { updatePlaybackState(mIsPlaying); } // Helper function to increment mPlaybackSpeed when necessary. The mPlaybackSpeed will control // the UI of fast forward button in control row. private void incrementFastForwardPlaybackSpeed() { switch (mPlaybackSpeed) { case PLAYBACK_SPEED_FAST_L0: case PLAYBACK_SPEED_FAST_L1: case PLAYBACK_SPEED_FAST_L2: case PLAYBACK_SPEED_FAST_L3: mPlaybackSpeed++; break; default: mPlaybackSpeed = PLAYBACK_SPEED_FAST_L0; break; } } // Helper function to decrement mPlaybackSpeed when necessary. The mPlaybackSpeed will control // the UI of rewind button in control row. private void decrementRewindPlaybackSpeed() { switch (mPlaybackSpeed) { case -PLAYBACK_SPEED_FAST_L0: case -PLAYBACK_SPEED_FAST_L1: case -PLAYBACK_SPEED_FAST_L2: case -PLAYBACK_SPEED_FAST_L3: mPlaybackSpeed--; break; default: mPlaybackSpeed = -PLAYBACK_SPEED_FAST_L0; break; } } /** * Called when the given action is invoked, either by click or key event. */ boolean dispatchAction(Action action, KeyEvent keyEvent) { boolean handled = false; if (action == mPlayPauseAction) { 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 ? mPlaybackSpeed == PLAYBACK_SPEED_NORMAL : mPlaybackSpeed != PLAYBACK_SPEED_PAUSED)) { pause(); } else if (canPlay && mPlaybackSpeed != PLAYBACK_SPEED_NORMAL) { play(); } onUpdatePlaybackStatusAfterUserAction(); handled = true; } else if (action == mSkipNextAction) { next(); handled = true; } else if (action == mSkipPreviousAction) { previous(); handled = true; } else if (action == mFastForwardAction) { if (mPlayerAdapter.isPrepared() && mPlaybackSpeed < getMaxForwardSpeedId()) { // When the customized fast forward action is available, it will be executed // when fast forward button is pressed. If current media item is not playing, the UI // will be updated to PLAYING status. if (mIsCustomizedFastForwardSupported) { // Change UI to Playing status. mIsPlaying = true; // Execute customized fast forward action. mPlayerAdapter.fastForward(); } else { // When the customized fast forward action is not supported, the fakePause // operation is needed to stop the media item but still indicating the media // item is playing from the UI perspective // Also the fakePause() method must be called before // incrementFastForwardPlaybackSpeed() method to make sure fake fast forward // computation is accurate. fakePause(); } // Change mPlaybackSpeed to control the UI. incrementFastForwardPlaybackSpeed(); onUpdatePlaybackStatusAfterUserAction(); } handled = true; } else if (action == mRewindAction) { if (mPlayerAdapter.isPrepared() && mPlaybackSpeed > -getMaxRewindSpeedId()) { if (mIsCustomizedFastForwardSupported) { mIsPlaying = true; mPlayerAdapter.rewind(); } else { fakePause(); } decrementRewindPlaybackSpeed(); onUpdatePlaybackStatusAfterUserAction(); } handled = true; } return handled; } @Override protected void onPlayStateChanged() { if (DEBUG) Log.v(TAG, "onStateChanged"); onUpdatePlaybackState(); super.onPlayStateChanged(); } @Override protected void onPlayCompleted() { super.onPlayCompleted(); mIsPlaying = false; mPlaybackSpeed = PLAYBACK_SPEED_PAUSED; mStartPosition = getCurrentPosition(); mStartTime = System.currentTimeMillis(); onUpdatePlaybackState(); } void onUpdatePlaybackState() { updatePlaybackState(mIsPlaying); } private void updatePlaybackState(boolean isPlaying) { if (mControlsRow == null) { return; } if (!isPlaying) { onUpdateProgress(); mPlayerAdapter.setProgressUpdatingEnabled(false); } else { mPlayerAdapter.setProgressUpdatingEnabled(true); } if (mFadeWhenPlaying && getHost() != null) { getHost().setControlsOverlayAutoHideEnabled(isPlaying); } final ArrayObjectAdapter primaryActionsAdapter = (ArrayObjectAdapter) getControlsRow().getPrimaryActionsAdapter(); if (mPlayPauseAction != null) { int index = !isPlaying ? PlaybackControlsRow.PlayPauseAction.PLAY : PlaybackControlsRow.PlayPauseAction.PAUSE; if (mPlayPauseAction.getIndex() != index) { mPlayPauseAction.setIndex(index); notifyItemChanged(primaryActionsAdapter, mPlayPauseAction); } } if (mFastForwardAction != null) { int index = 0; if (mPlaybackSpeed >= PLAYBACK_SPEED_FAST_L0) { index = mPlaybackSpeed - PLAYBACK_SPEED_FAST_L0 + 1; } if (mFastForwardAction.getIndex() != index) { mFastForwardAction.setIndex(index); notifyItemChanged(primaryActionsAdapter, mFastForwardAction); } } if (mRewindAction != null) { int index = 0; if (mPlaybackSpeed <= -PLAYBACK_SPEED_FAST_L0) { index = -mPlaybackSpeed - PLAYBACK_SPEED_FAST_L0 + 1; } if (mRewindAction.getIndex() != index) { mRewindAction.setIndex(index); notifyItemChanged(primaryActionsAdapter, mRewindAction); } } } /** * Returns the fast forward speeds. */ @NonNull public int[] getFastForwardSpeeds() { return mFastForwardSpeeds; } /** * Returns the rewind speeds. */ @NonNull public int[] getRewindSpeeds() { return mRewindSpeeds; } private int getMaxForwardSpeedId() { return PLAYBACK_SPEED_FAST_L0 + (mFastForwardSpeeds.length - 1); } private int getMaxRewindSpeedId() { return PLAYBACK_SPEED_FAST_L0 + (mRewindSpeeds.length - 1); } /** * Gets current position of the player. If the player is playing/paused, this * method returns current position from {@link PlayerAdapter}. Otherwise, if the player is * fastforwarding/rewinding, the method fake-pauses the {@link PlayerAdapter} and returns its * own calculated position. * @return Current position of the player. */ @Override public long getCurrentPosition() { int speed; if (mPlaybackSpeed == PlaybackControlGlue.PLAYBACK_SPEED_PAUSED || mPlaybackSpeed == PlaybackControlGlue.PLAYBACK_SPEED_NORMAL) { // If the adapter is playing/paused, using the position from adapter instead. return mPlayerAdapter.getCurrentPosition(); } else if (mPlaybackSpeed >= PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) { // If fast forward operation is supported in this scenario, current player position // can be get from mPlayerAdapter.getCurrentPosition() directly if (mIsCustomizedFastForwardSupported) { return mPlayerAdapter.getCurrentPosition(); } int index = mPlaybackSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0; speed = getFastForwardSpeeds()[index]; } else if (mPlaybackSpeed <= -PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) { // If fast rewind is supported in this scenario, current player position // can be get from mPlayerAdapter.getCurrentPosition() directly if (mIsCustomizedRewindSupported) { return mPlayerAdapter.getCurrentPosition(); } int index = -mPlaybackSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0; speed = -getRewindSpeeds()[index]; } else { return -1; } long position = mStartPosition + (System.currentTimeMillis() - mStartTime) * speed; if (position > getDuration()) { mPlaybackSpeed = PLAYBACK_SPEED_PAUSED; position = getDuration(); mPlayerAdapter.seekTo(position); mStartPosition = 0; pause(); } else if (position < 0) { mPlaybackSpeed = PLAYBACK_SPEED_PAUSED; position = 0; mPlayerAdapter.seekTo(position); mStartPosition = 0; pause(); } return position; } @Override public void play() { if (!mPlayerAdapter.isPrepared()) { return; } // Solves the situation that a player pause at the end and click play button. At this case // the player will restart from the beginning. if (mPlaybackSpeed == PLAYBACK_SPEED_PAUSED && mPlayerAdapter.getCurrentPosition() >= mPlayerAdapter.getDuration()) { mStartPosition = 0; } else { mStartPosition = getCurrentPosition(); } mStartTime = System.currentTimeMillis(); mIsPlaying = true; mPlaybackSpeed = PLAYBACK_SPEED_NORMAL; mPlayerAdapter.seekTo(mStartPosition); super.play(); onUpdatePlaybackState(); } @Override public void pause() { mIsPlaying = false; mPlaybackSpeed = PLAYBACK_SPEED_PAUSED; mStartPosition = getCurrentPosition(); mStartTime = System.currentTimeMillis(); super.pause(); onUpdatePlaybackState(); } /** * Control row shows PLAY, but the media is actually paused when the player is * fastforwarding/rewinding. */ private void fakePause() { mIsPlaying = true; mStartPosition = getCurrentPosition(); mStartTime = System.currentTimeMillis(); super.pause(); onUpdatePlaybackState(); } }