/* * 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 android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; import android.content.Context; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.RestrictTo; 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.ObjectAdapter; import android.support.v17.leanback.widget.PlaybackControlsRow; import android.support.v17.leanback.widget.PlaybackControlsRowPresenter; import android.support.v17.leanback.widget.PlaybackRowPresenter; import android.support.v17.leanback.widget.RowPresenter; import android.util.Log; import android.view.KeyEvent; import android.view.View; 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()}. *

* * @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 = 0x1; /** * The adapter key for the skip to previous control. */ public static final int ACTION_SKIP_TO_PREVIOUS = 0x10; /** * The adapter key for the rewind control. */ public static final int ACTION_REWIND = 0x20; /** * The adapter key for the play/pause control. */ public static final int ACTION_PLAY_PAUSE = 0x40; /** * The adapter key for the fast forward control. */ public static final int ACTION_FAST_FORWARD = 0x80; /** * The adapter key for the skip to next control. */ public static final int ACTION_SKIP_TO_NEXT = 0x100; /** * 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 = 0x1000; /** @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.PlayPauseAction mPlayPauseAction; 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; /** * 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; } @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); } /** * 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()) { fakePause(); 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; } onUpdatePlaybackStatusAfterUserAction(); } handled = true; } else if (action == mRewindAction) { if (mPlayerAdapter.isPrepared() && mPlaybackSpeed > -getMaxRewindSpeedId()) { fakePause(); 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; } 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) { int index = mPlaybackSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0; speed = getFastForwardSpeeds()[index]; } else if (mPlaybackSpeed <= -PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) { 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; } /** * Returns a bitmask of actions supported by the media player. * @see ACTION_ for constants that may be returned by this method. */ public long getSupportedActions() { return PlaybackBannerControlGlue.ACTION_PLAY_PAUSE; } @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(); } }