Merge "Add PlaybackControlGlue for support v4 fragment" into mnc-ub-dev
diff --git a/v17/leanback/api/current.txt b/v17/leanback/api/current.txt
index e6494fa..2c2fdba 100644
--- a/v17/leanback/api/current.txt
+++ b/v17/leanback/api/current.txt
@@ -378,6 +378,61 @@
field public static final int PLAYBACK_SPEED_PAUSED = 0; // 0x0
}
+ public abstract class PlaybackControlSupportGlue implements android.support.v17.leanback.widget.OnActionClickedListener android.view.View.OnKeyListener {
+ ctor public PlaybackControlSupportGlue(android.content.Context, int[]);
+ ctor public PlaybackControlSupportGlue(android.content.Context, int[], int[]);
+ ctor public PlaybackControlSupportGlue(android.content.Context, android.support.v17.leanback.app.PlaybackOverlaySupportFragment, int[]);
+ ctor public PlaybackControlSupportGlue(android.content.Context, android.support.v17.leanback.app.PlaybackOverlaySupportFragment, int[], int[]);
+ method public android.support.v17.leanback.widget.PlaybackControlsRowPresenter createControlsRowAndPresenter();
+ method protected android.support.v17.leanback.widget.SparseArrayObjectAdapter createPrimaryActionsAdapter(android.support.v17.leanback.widget.PresenterSelector);
+ method public void enableProgressUpdating(boolean);
+ method public android.content.Context getContext();
+ method public android.support.v17.leanback.widget.PlaybackControlsRow getControlsRow();
+ method public abstract int getCurrentPosition();
+ method public abstract int getCurrentSpeedId();
+ method public int[] getFastForwardSpeeds();
+ method public android.support.v17.leanback.app.PlaybackOverlaySupportFragment getFragment();
+ method public abstract android.graphics.drawable.Drawable getMediaArt();
+ method public abstract int getMediaDuration();
+ method public abstract java.lang.CharSequence getMediaSubtitle();
+ method public abstract java.lang.CharSequence getMediaTitle();
+ method public android.support.v17.leanback.widget.OnItemViewClickedListener getOnItemViewClickedListener();
+ method public int[] getRewindSpeeds();
+ method public abstract long getSupportedActions();
+ method public int getUpdatePeriod();
+ method public abstract boolean hasValidMedia();
+ method public boolean isFadingEnabled();
+ method public abstract boolean isMediaPlaying();
+ method public void onActionClicked(android.support.v17.leanback.widget.Action);
+ method public boolean onKey(android.view.View, int, android.view.KeyEvent);
+ method protected void onMetadataChanged();
+ method protected abstract void onRowChanged(android.support.v17.leanback.widget.PlaybackControlsRow);
+ method protected void onStateChanged();
+ method protected abstract void pausePlayback();
+ method public void setControlsRow(android.support.v17.leanback.widget.PlaybackControlsRow);
+ method public void setFadingEnabled(boolean);
+ method public deprecated void setOnItemViewClickedListener(android.support.v17.leanback.widget.OnItemViewClickedListener);
+ method protected abstract void skipToNext();
+ method protected abstract void skipToPrevious();
+ method protected abstract void startPlayback(int);
+ method public void updateProgress();
+ field public static final int ACTION_CUSTOM_LEFT_FIRST = 1; // 0x1
+ field public static final int ACTION_CUSTOM_RIGHT_FIRST = 4096; // 0x1000
+ field public static final int ACTION_FAST_FORWARD = 128; // 0x80
+ field public static final int ACTION_PLAY_PAUSE = 64; // 0x40
+ field public static final int ACTION_REWIND = 32; // 0x20
+ field public static final int ACTION_SKIP_TO_NEXT = 256; // 0x100
+ field public static final int ACTION_SKIP_TO_PREVIOUS = 16; // 0x10
+ field public static final int PLAYBACK_SPEED_FAST_L0 = 10; // 0xa
+ field public static final int PLAYBACK_SPEED_FAST_L1 = 11; // 0xb
+ field public static final int PLAYBACK_SPEED_FAST_L2 = 12; // 0xc
+ field public static final int PLAYBACK_SPEED_FAST_L3 = 13; // 0xd
+ field public static final int PLAYBACK_SPEED_FAST_L4 = 14; // 0xe
+ field public static final int PLAYBACK_SPEED_INVALID = -1; // 0xffffffff
+ field public static final int PLAYBACK_SPEED_NORMAL = 1; // 0x1
+ field public static final int PLAYBACK_SPEED_PAUSED = 0; // 0x0
+ }
+
public class PlaybackOverlayFragment extends android.support.v17.leanback.app.DetailsFragment {
ctor public PlaybackOverlayFragment();
method public int getBackgroundType();
diff --git a/v17/leanback/generatev4.py b/v17/leanback/generatev4.py
index ae2dddd..1b60b09e 100755
--- a/v17/leanback/generatev4.py
+++ b/v17/leanback/generatev4.py
@@ -42,3 +42,13 @@
outfile.write(line)
file.close()
outfile.close()
+
+file = open('src/android/support/v17/leanback/app/PlaybackControlGlue.java', 'r')
+outfile = open('src/android/support/v17/leanback/app/PlaybackControlSupportGlue.java', 'w')
+outfile.write("/* This file is auto-generated from PlaybackControlGlue.java. DO NOT MODIFY. */\n\n")
+for line in file:
+ line = line.replace('PlaybackControlGlue', 'PlaybackControlSupportGlue');
+ line = line.replace('PlaybackOverlayFragment', 'PlaybackOverlaySupportFragment');
+ outfile.write(line)
+file.close()
+outfile.close()
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PlaybackControlSupportGlue.java b/v17/leanback/src/android/support/v17/leanback/app/PlaybackControlSupportGlue.java
new file mode 100644
index 0000000..76f312d
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/app/PlaybackControlSupportGlue.java
@@ -0,0 +1,858 @@
+/* This file is auto-generated from PlaybackControlGlue.java. DO NOT MODIFY. */
+
+package android.support.v17.leanback.app;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Message;
+import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter;
+import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.ControlButtonPresenterSelector;
+import android.support.v17.leanback.widget.OnActionClickedListener;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.PlaybackControlsRow;
+import android.support.v17.leanback.widget.PlaybackControlsRowPresenter;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.PresenterSelector;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+import android.util.Log;
+import android.view.InputEvent;
+import android.view.KeyEvent;
+import android.view.View;
+
+
+/**
+ * A helper class for managing a {@link android.support.v17.leanback.widget.PlaybackControlsRow} and
+ * {@link PlaybackOverlaySupportFragment} that implements a recommended approach to handling standard
+ * playback control actions such as play/pause, fast forward/rewind at progressive speed levels,
+ * and skip to next/previous. This helper class is a glue layer in that it manages the
+ * configuration of and interaction between the leanback UI components by defining a functional
+ * interface to the media player.
+ *
+ * <p>You can instantiate a concrete subclass such as {@link MediaControllerGlue} or you must
+ * subclass this abstract helper. To create a subclass you must implement all of the
+ * abstract methods and the subclass must invoke {@link #onMetadataChanged()} and
+ * {@link #onStateChanged()} appropriately.
+ * </p>
+ *
+ * <p>To use an instance of the glue layer, first construct an instance. Constructor parameters
+ * inform the glue what speed levels are supported for fast forward/rewind. Providing a
+ * {@link android.support.v17.leanback.app.PlaybackOverlaySupportFragment} is optional.
+ * </p>
+ *
+ * <p>If you have your own controls row you must pass it to {@link #setControlsRow}.
+ * The row will be updated by the glue layer based on the media metadata and playback state.
+ * Alternatively, you may call {@link #createControlsRowAndPresenter()} which will set a controls
+ * row and return a row presenter you can use to present the row.
+ * </p>
+ *
+ * <p>The helper sets a {@link android.support.v17.leanback.widget.SparseArrayObjectAdapter}
+ * on the controls row as the primary actions adapter, and adds actions to it. You can provide
+ * additional actions by overriding {@link #createPrimaryActionsAdapter}. This helper does not
+ * deal in secondary actions so those you may add separately.
+ * </p>
+ *
+ * <p>Provide a click listener on your fragment and if an action is clicked, call
+ * {@link #onActionClicked}. There is no need to call {@link #setOnItemViewClickedListener}
+ * but if you do a click listener will be installed on the fragment and recognized action clicks
+ * will be handled. Your listener will be called only for unhandled actions.
+ * </p>
+ *
+ * <p>The helper implements a key event handler. If you pass a
+ * {@link android.support.v17.leanback.app.PlaybackOverlaySupportFragment} the fragment's input event
+ * handler will be set. Otherwise, you should set the glue object as key event handler to the
+ * ViewHolder when bound by your row presenter; see
+ * {@link RowPresenter.ViewHolder#setOnKeyListener(android.view.View.OnKeyListener)}.
+ * </p>
+ *
+ * <p>To update the controls row progress during playback, override {@link #enableProgressUpdating}
+ * to manage the lifecycle of a periodic callback to {@link #updateProgress()}.
+ * {@link #getUpdatePeriod()} provides a recommended update period.
+ * </p>
+ *
+ */
+public abstract class PlaybackControlSupportGlue implements OnActionClickedListener, View.OnKeyListener {
+ /**
+ * The adapter key for the first custom control on the right 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;
+
+ /**
+ * 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 = "PlaybackControlSupportGlue";
+ private static final boolean DEBUG = false;
+
+ private static final int MSG_UPDATE_PLAYBACK_STATE = 100;
+ private static final int UPDATE_PLAYBACK_STATE_DELAY_MS = 2000;
+ private static final int NUMBER_OF_SEEK_SPEEDS = PLAYBACK_SPEED_FAST_L4 -
+ PLAYBACK_SPEED_FAST_L0 + 1;
+
+ private final PlaybackOverlaySupportFragment mFragment;
+ private final Context mContext;
+ private final int[] mFastForwardSpeeds;
+ private final int[] mRewindSpeeds;
+ private PlaybackControlsRow mControlsRow;
+ private SparseArrayObjectAdapter mPrimaryActionsAdapter;
+ private PlaybackControlsRow.PlayPauseAction mPlayPauseAction;
+ private PlaybackControlsRow.SkipNextAction mSkipNextAction;
+ private PlaybackControlsRow.SkipPreviousAction mSkipPreviousAction;
+ private PlaybackControlsRow.FastForwardAction mFastForwardAction;
+ private PlaybackControlsRow.RewindAction mRewindAction;
+ private OnItemViewClickedListener mExternalOnItemViewClickedListener;
+ private int mPlaybackSpeed = PLAYBACK_SPEED_NORMAL;
+ private boolean mFadeWhenPlaying = true;
+
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == MSG_UPDATE_PLAYBACK_STATE) {
+ updatePlaybackState();
+ }
+ }
+ };
+
+ private final OnItemViewClickedListener mOnItemViewClickedListener =
+ new OnItemViewClickedListener() {
+ @Override
+ public void onItemClicked(Presenter.ViewHolder viewHolder, Object object,
+ RowPresenter.ViewHolder viewHolder2, Row row) {
+ if (DEBUG) Log.v(TAG, "onItemClicked " + object);
+ boolean handled = false;
+ if (object instanceof Action) {
+ handled = dispatchAction((Action) object, null);
+ }
+ if (!handled && mExternalOnItemViewClickedListener != null) {
+ mExternalOnItemViewClickedListener.onItemClicked(viewHolder, object,
+ viewHolder2, row);
+ }
+ }
+ };
+
+ /**
+ * Constructor for the glue.
+ *
+ * @param context
+ * @param seekSpeeds Array of seek speeds for fast forward and rewind.
+ */
+ public PlaybackControlSupportGlue(Context context, int[] seekSpeeds) {
+ this(context, null, seekSpeeds, seekSpeeds);
+ }
+
+ /**
+ * Constructor for the glue.
+ *
+ * @param context
+ * @param fastForwardSpeeds Array of seek speeds for fast forward.
+ * @param rewindSpeeds Array of seek speeds for rewind.
+ */
+ public PlaybackControlSupportGlue(Context context,
+ int[] fastForwardSpeeds,
+ int[] rewindSpeeds) {
+ this(context, null, fastForwardSpeeds, rewindSpeeds);
+ }
+
+ /**
+ * Constructor for the glue.
+ *
+ * @param context
+ * @param fragment Optional; if using a {@link PlaybackOverlaySupportFragment}, pass it in.
+ * @param seekSpeeds Array of seek speeds for fast forward and rewind.
+ */
+ public PlaybackControlSupportGlue(Context context,
+ PlaybackOverlaySupportFragment fragment,
+ int[] seekSpeeds) {
+ this(context, fragment, seekSpeeds, seekSpeeds);
+ }
+
+ /**
+ * Constructor for the glue.
+ *
+ * @param context
+ * @param fragment Optional; if using a {@link PlaybackOverlaySupportFragment}, pass it in.
+ * @param fastForwardSpeeds Array of seek speeds for fast forward.
+ * @param rewindSpeeds Array of seek speeds for rewind.
+ */
+ public PlaybackControlSupportGlue(Context context,
+ PlaybackOverlaySupportFragment fragment,
+ int[] fastForwardSpeeds,
+ int[] rewindSpeeds) {
+ mContext = context;
+ mFragment = fragment;
+ if (fragment != null) {
+ attachToFragment();
+ }
+ if (fastForwardSpeeds.length == 0 || fastForwardSpeeds.length > NUMBER_OF_SEEK_SPEEDS) {
+ throw new IllegalStateException("invalid fastForwardSpeeds array size");
+ }
+ mFastForwardSpeeds = fastForwardSpeeds;
+ if (rewindSpeeds.length == 0 || rewindSpeeds.length > NUMBER_OF_SEEK_SPEEDS) {
+ throw new IllegalStateException("invalid rewindSpeeds array size");
+ }
+ mRewindSpeeds = rewindSpeeds;
+ }
+
+ private final PlaybackOverlaySupportFragment.InputEventHandler mOnInputEventHandler =
+ new PlaybackOverlaySupportFragment.InputEventHandler() {
+ @Override
+ public boolean handleInputEvent(InputEvent event) {
+ if (event instanceof KeyEvent) {
+ KeyEvent keyEvent = (KeyEvent) event;
+ return onKey(null, keyEvent.getKeyCode(), keyEvent);
+ }
+ return false;
+ }
+ };
+
+ private void attachToFragment() {
+ mFragment.setInputEventHandler(mOnInputEventHandler);
+ }
+
+ /**
+ * Helper method for instantiating a
+ * {@link android.support.v17.leanback.widget.PlaybackControlsRow} and corresponding
+ * {@link android.support.v17.leanback.widget.PlaybackControlsRowPresenter}.
+ */
+ public PlaybackControlsRowPresenter createControlsRowAndPresenter() {
+ PlaybackControlsRow controlsRow = new PlaybackControlsRow(this);
+ setControlsRow(controlsRow);
+
+ AbstractDetailsDescriptionPresenter detailsPresenter =
+ new AbstractDetailsDescriptionPresenter() {
+ @Override
+ protected void onBindDescription(AbstractDetailsDescriptionPresenter.ViewHolder
+ viewHolder, Object object) {
+ PlaybackControlSupportGlue glue = (PlaybackControlSupportGlue) object;
+ if (glue.hasValidMedia()) {
+ viewHolder.getTitle().setText(glue.getMediaTitle());
+ viewHolder.getSubtitle().setText(glue.getMediaSubtitle());
+ } else {
+ viewHolder.getTitle().setText("");
+ viewHolder.getSubtitle().setText("");
+ }
+ }
+ };
+ return new PlaybackControlsRowPresenter(detailsPresenter) {
+ @Override
+ protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) {
+ super.onBindRowViewHolder(vh, item);
+ vh.setOnKeyListener(PlaybackControlSupportGlue.this);
+ }
+ @Override
+ protected void onUnbindRowViewHolder(RowPresenter.ViewHolder vh) {
+ super.onUnbindRowViewHolder(vh);
+ vh.setOnKeyListener(null);
+ }
+ };
+ }
+
+ /**
+ * Returns the fragment.
+ */
+ public PlaybackOverlaySupportFragment getFragment() {
+ return mFragment;
+ }
+
+ /**
+ * Returns the context.
+ */
+ public Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Returns the fast forward speeds.
+ */
+ public int[] getFastForwardSpeeds() {
+ return mFastForwardSpeeds;
+ }
+
+ /**
+ * Returns the rewind speeds.
+ */
+ public int[] getRewindSpeeds() {
+ return mRewindSpeeds;
+ }
+
+ /**
+ * Sets the controls to fade after a timeout when media is playing.
+ */
+ public void setFadingEnabled(boolean enable) {
+ mFadeWhenPlaying = enable;
+ if (!mFadeWhenPlaying && mFragment != null) {
+ mFragment.setFadingEnabled(false);
+ }
+ }
+
+ /**
+ * Returns true if controls are set to fade when media is playing.
+ */
+ public boolean isFadingEnabled() {
+ return mFadeWhenPlaying;
+ }
+
+ /**
+ * Set the {@link OnItemViewClickedListener} to be called if the click event
+ * is not handled internally.
+ * @param listener
+ * @deprecated Don't call this. Instead set the listener on the fragment yourself,
+ * and call {@link #onActionClicked} to handle clicks.
+ */
+ public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
+ mExternalOnItemViewClickedListener = listener;
+ if (mFragment != null) {
+ mFragment.setOnItemViewClickedListener(mOnItemViewClickedListener);
+ }
+ }
+
+ /**
+ * Returns the {@link OnItemViewClickedListener}.
+ */
+ public OnItemViewClickedListener getOnItemViewClickedListener() {
+ return mExternalOnItemViewClickedListener;
+ }
+
+ /**
+ * Sets the controls row to be managed by the glue layer.
+ * The primary actions and playback state related aspects of the row
+ * are updated by the glue.
+ */
+ public void setControlsRow(PlaybackControlsRow controlsRow) {
+ mControlsRow = controlsRow;
+ mPrimaryActionsAdapter = createPrimaryActionsAdapter(
+ new ControlButtonPresenterSelector());
+ mControlsRow.setPrimaryActionsAdapter(mPrimaryActionsAdapter);
+ updateControlsRow();
+ }
+
+ /**
+ * Returns the playback controls row managed by the glue layer.
+ */
+ public PlaybackControlsRow getControlsRow() {
+ return mControlsRow;
+ }
+
+ /**
+ * Override this to start/stop a runnable to call {@link #updateProgress} at
+ * an interval such as {@link #getUpdatePeriod}.
+ */
+ public void enableProgressUpdating(boolean enable) {
+ }
+
+ /**
+ * Returns the time period in milliseconds that should be used
+ * to update the progress. See {@link #updateProgress()}.
+ */
+ public int getUpdatePeriod() {
+ // TODO: calculate a better update period based on total duration and screen size
+ return 500;
+ }
+
+ /**
+ * Updates the progress bar based on the current media playback position.
+ */
+ public void updateProgress() {
+ int position = getCurrentPosition();
+ if (DEBUG) Log.v(TAG, "updateProgress " + position);
+ mControlsRow.setCurrentTime(position);
+ }
+
+ /**
+ * 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) {
+ mPlaybackSpeed = PLAYBACK_SPEED_NORMAL;
+ startPlayback(mPlaybackSpeed);
+ updatePlaybackStatusAfterUserAction();
+ return keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE;
+ }
+ return false;
+ }
+ Action action = mControlsRow.getActionForKeyCode(mPrimaryActionsAdapter, keyCode);
+ if (action != null) {
+ if (action == mPrimaryActionsAdapter.lookup(ACTION_PLAY_PAUSE) ||
+ action == mPrimaryActionsAdapter.lookup(ACTION_REWIND) ||
+ action == mPrimaryActionsAdapter.lookup(ACTION_FAST_FORWARD) ||
+ action == mPrimaryActionsAdapter.lookup(ACTION_SKIP_TO_PREVIOUS) ||
+ action == mPrimaryActionsAdapter.lookup(ACTION_SKIP_TO_NEXT)) {
+ if (((KeyEvent) event).getAction() == KeyEvent.ACTION_DOWN) {
+ dispatchAction(action, (KeyEvent) event);
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called when the given action is invoked, either by click or keyevent.
+ */
+ private 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;
+ if (mPlaybackSpeed != PLAYBACK_SPEED_NORMAL) {
+ if (canPlay) {
+ mPlaybackSpeed = PLAYBACK_SPEED_NORMAL;
+ startPlayback(mPlaybackSpeed);
+ }
+ } else if (canPause) {
+ mPlaybackSpeed = PLAYBACK_SPEED_PAUSED;
+ pausePlayback();
+ }
+ updatePlaybackStatusAfterUserAction();
+ handled = true;
+ } else if (action == mSkipNextAction) {
+ skipToNext();
+ handled = true;
+ } else if (action == mSkipPreviousAction) {
+ skipToPrevious();
+ handled = true;
+ } else if (action == mFastForwardAction) {
+ if (mPlaybackSpeed < getMaxForwardSpeedId()) {
+ 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;
+ }
+ startPlayback(mPlaybackSpeed);
+ updatePlaybackStatusAfterUserAction();
+ }
+ handled = true;
+ } else if (action == mRewindAction) {
+ if (mPlaybackSpeed > -getMaxRewindSpeedId()) {
+ 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;
+ }
+ startPlayback(mPlaybackSpeed);
+ updatePlaybackStatusAfterUserAction();
+ }
+ handled = true;
+ }
+ return handled;
+ }
+
+ private int getMaxForwardSpeedId() {
+ return PLAYBACK_SPEED_FAST_L0 + (mFastForwardSpeeds.length - 1);
+ }
+
+ private int getMaxRewindSpeedId() {
+ return PLAYBACK_SPEED_FAST_L0 + (mRewindSpeeds.length - 1);
+ }
+
+ private void updateControlsRow() {
+ updateRowMetadata();
+ mHandler.removeMessages(MSG_UPDATE_PLAYBACK_STATE);
+ updatePlaybackState();
+ }
+
+ private void updatePlaybackStatusAfterUserAction() {
+ updatePlaybackState(mPlaybackSpeed);
+ // Sync playback state after a delay
+ mHandler.removeMessages(MSG_UPDATE_PLAYBACK_STATE);
+ mHandler.sendEmptyMessageDelayed(MSG_UPDATE_PLAYBACK_STATE,
+ UPDATE_PLAYBACK_STATE_DELAY_MS);
+ }
+
+ private void updateRowMetadata() {
+ if (mControlsRow == null) {
+ return;
+ }
+
+ if (DEBUG) Log.v(TAG, "updateRowMetadata hasValidMedia " + hasValidMedia());
+
+ if (!hasValidMedia()) {
+ mControlsRow.setImageDrawable(null);
+ mControlsRow.setTotalTime(0);
+ mControlsRow.setCurrentTime(0);
+ } else {
+ mControlsRow.setImageDrawable(getMediaArt());
+ mControlsRow.setTotalTime(getMediaDuration());
+ mControlsRow.setCurrentTime(getCurrentPosition());
+ }
+
+ onRowChanged(mControlsRow);
+ }
+
+ private void updatePlaybackState() {
+ if (hasValidMedia()) {
+ mPlaybackSpeed = getCurrentSpeedId();
+ updatePlaybackState(mPlaybackSpeed);
+ }
+ }
+
+ private void updatePlaybackState(int playbackSpeed) {
+ if (mControlsRow == null) {
+ return;
+ }
+
+ final long actions = getSupportedActions();
+ if ((actions & ACTION_SKIP_TO_PREVIOUS) != 0) {
+ if (mSkipPreviousAction == null) {
+ mSkipPreviousAction = new PlaybackControlsRow.SkipPreviousAction(mContext);
+ }
+ mPrimaryActionsAdapter.set(ACTION_SKIP_TO_PREVIOUS, mSkipPreviousAction);
+ } else {
+ mPrimaryActionsAdapter.clear(ACTION_SKIP_TO_PREVIOUS);
+ mSkipPreviousAction = null;
+ }
+ if ((actions & ACTION_REWIND) != 0) {
+ if (mRewindAction == null) {
+ mRewindAction = new PlaybackControlsRow.RewindAction(mContext,
+ mRewindSpeeds.length);
+ }
+ mPrimaryActionsAdapter.set(ACTION_REWIND, mRewindAction);
+ } else {
+ mPrimaryActionsAdapter.clear(ACTION_REWIND);
+ mRewindAction = null;
+ }
+ if ((actions & ACTION_PLAY_PAUSE) != 0) {
+ if (mPlayPauseAction == null) {
+ mPlayPauseAction = new PlaybackControlsRow.PlayPauseAction(mContext);
+ }
+ mPrimaryActionsAdapter.set(ACTION_PLAY_PAUSE, mPlayPauseAction);
+ } else {
+ mPrimaryActionsAdapter.clear(ACTION_PLAY_PAUSE);
+ mPlayPauseAction = null;
+ }
+ if ((actions & ACTION_FAST_FORWARD) != 0) {
+ if (mFastForwardAction == null) {
+ mFastForwardAction = new PlaybackControlsRow.FastForwardAction(mContext,
+ mFastForwardSpeeds.length);
+ }
+ mPrimaryActionsAdapter.set(ACTION_FAST_FORWARD, mFastForwardAction);
+ } else {
+ mPrimaryActionsAdapter.clear(ACTION_FAST_FORWARD);
+ mFastForwardAction = null;
+ }
+ if ((actions & ACTION_SKIP_TO_NEXT) != 0) {
+ if (mSkipNextAction == null) {
+ mSkipNextAction = new PlaybackControlsRow.SkipNextAction(mContext);
+ }
+ mPrimaryActionsAdapter.set(ACTION_SKIP_TO_NEXT, mSkipNextAction);
+ } else {
+ mPrimaryActionsAdapter.clear(ACTION_SKIP_TO_NEXT);
+ mSkipNextAction = null;
+ }
+
+ if (mFastForwardAction != null) {
+ int index = 0;
+ if (playbackSpeed >= PLAYBACK_SPEED_FAST_L0) {
+ index = playbackSpeed - PLAYBACK_SPEED_FAST_L0;
+ if (playbackSpeed < getMaxForwardSpeedId()) {
+ index++;
+ }
+ }
+ if (mFastForwardAction.getIndex() != index) {
+ mFastForwardAction.setIndex(index);
+ notifyItemChanged(mPrimaryActionsAdapter, mFastForwardAction);
+ }
+ }
+ if (mRewindAction != null) {
+ int index = 0;
+ if (playbackSpeed <= -PLAYBACK_SPEED_FAST_L0) {
+ index = -playbackSpeed - PLAYBACK_SPEED_FAST_L0;
+ if (-playbackSpeed < getMaxRewindSpeedId()) {
+ index++;
+ }
+ }
+ if (mRewindAction.getIndex() != index) {
+ mRewindAction.setIndex(index);
+ notifyItemChanged(mPrimaryActionsAdapter, mRewindAction);
+ }
+ }
+
+ if (playbackSpeed == PLAYBACK_SPEED_PAUSED) {
+ updateProgress();
+ enableProgressUpdating(false);
+ } else {
+ enableProgressUpdating(true);
+ }
+
+ if (mFadeWhenPlaying && mFragment != null) {
+ mFragment.setFadingEnabled(playbackSpeed == PLAYBACK_SPEED_NORMAL);
+ }
+
+ if (mPlayPauseAction != null) {
+ int index = playbackSpeed == PLAYBACK_SPEED_PAUSED ?
+ PlaybackControlsRow.PlayPauseAction.PLAY :
+ PlaybackControlsRow.PlayPauseAction.PAUSE;
+ if (mPlayPauseAction.getIndex() != index) {
+ mPlayPauseAction.setIndex(index);
+ notifyItemChanged(mPrimaryActionsAdapter, mPlayPauseAction);
+ }
+ }
+ }
+
+ private static void notifyItemChanged(SparseArrayObjectAdapter adapter, Object object) {
+ int index = adapter.indexOf(object);
+ if (index >= 0) {
+ adapter.notifyArrayItemRangeChanged(index, 1);
+ }
+ }
+
+ private static String getSpeedString(int speed) {
+ switch (speed) {
+ case PLAYBACK_SPEED_INVALID:
+ return "PLAYBACK_SPEED_INVALID";
+ case PLAYBACK_SPEED_PAUSED:
+ return "PLAYBACK_SPEED_PAUSED";
+ case PLAYBACK_SPEED_NORMAL:
+ return "PLAYBACK_SPEED_NORMAL";
+ case PLAYBACK_SPEED_FAST_L0:
+ return "PLAYBACK_SPEED_FAST_L0";
+ case PLAYBACK_SPEED_FAST_L1:
+ return "PLAYBACK_SPEED_FAST_L1";
+ case PLAYBACK_SPEED_FAST_L2:
+ return "PLAYBACK_SPEED_FAST_L2";
+ case PLAYBACK_SPEED_FAST_L3:
+ return "PLAYBACK_SPEED_FAST_L3";
+ case PLAYBACK_SPEED_FAST_L4:
+ return "PLAYBACK_SPEED_FAST_L4";
+ case -PLAYBACK_SPEED_FAST_L0:
+ return "-PLAYBACK_SPEED_FAST_L0";
+ case -PLAYBACK_SPEED_FAST_L1:
+ return "-PLAYBACK_SPEED_FAST_L1";
+ case -PLAYBACK_SPEED_FAST_L2:
+ return "-PLAYBACK_SPEED_FAST_L2";
+ case -PLAYBACK_SPEED_FAST_L3:
+ return "-PLAYBACK_SPEED_FAST_L3";
+ case -PLAYBACK_SPEED_FAST_L4:
+ return "-PLAYBACK_SPEED_FAST_L4";
+ }
+ return null;
+ }
+
+ /**
+ * Returns true if there is a valid media item.
+ */
+ public abstract boolean hasValidMedia();
+
+ /**
+ * Returns true if media is currently playing.
+ */
+ public abstract boolean isMediaPlaying();
+
+ /**
+ * Returns the title of the media item.
+ */
+ public abstract CharSequence getMediaTitle();
+
+ /**
+ * Returns the subtitle of the media item.
+ */
+ public abstract CharSequence getMediaSubtitle();
+
+ /**
+ * Returns the duration of the media item in milliseconds.
+ */
+ public abstract int getMediaDuration();
+
+ /**
+ * Returns a bitmap of the art for the media item.
+ */
+ public abstract Drawable getMediaArt();
+
+ /**
+ * Returns a bitmask of actions supported by the media player.
+ */
+ public abstract long getSupportedActions();
+
+ /**
+ * Returns the current playback speed. When playing normally,
+ * {@link #PLAYBACK_SPEED_NORMAL} should be returned.
+ */
+ public abstract int getCurrentSpeedId();
+
+ /**
+ * Returns the current position of the media item in milliseconds.
+ */
+ public abstract int getCurrentPosition();
+
+ /**
+ * Start playback at the given speed.
+ * @param speed The desired playback speed. For normal playback this will be
+ * {@link #PLAYBACK_SPEED_NORMAL}; higher positive values for fast forward,
+ * and negative values for rewind.
+ */
+ protected abstract void startPlayback(int speed);
+
+ /**
+ * Pause playback.
+ */
+ protected abstract void pausePlayback();
+
+ /**
+ * Skip to the next track.
+ */
+ protected abstract void skipToNext();
+
+ /**
+ * Skip to the previous track.
+ */
+ protected abstract void skipToPrevious();
+
+ /**
+ * Invoked when the playback controls row has changed. The adapter containing this row
+ * should be notified.
+ */
+ protected abstract void onRowChanged(PlaybackControlsRow row);
+
+ /**
+ * Creates the primary action adapter. May be overridden to add additional primary
+ * actions to the adapter.
+ */
+ protected SparseArrayObjectAdapter createPrimaryActionsAdapter(
+ PresenterSelector presenterSelector) {
+ return new SparseArrayObjectAdapter(presenterSelector);
+ }
+
+ /**
+ * Must be called appropriately by a subclass when the playback state has changed.
+ */
+ protected void onStateChanged() {
+ if (DEBUG) Log.v(TAG, "onStateChanged");
+ // If a pending control button update is present, delay
+ // the update until the state settles.
+ if (!hasValidMedia()) {
+ return;
+ }
+ if (mHandler.hasMessages(MSG_UPDATE_PLAYBACK_STATE)) {
+ mHandler.removeMessages(MSG_UPDATE_PLAYBACK_STATE);
+ if (getCurrentSpeedId() != mPlaybackSpeed) {
+ if (DEBUG) Log.v(TAG, "Status expectation mismatch, delaying update");
+ mHandler.sendEmptyMessageDelayed(MSG_UPDATE_PLAYBACK_STATE,
+ UPDATE_PLAYBACK_STATE_DELAY_MS);
+ } else {
+ if (DEBUG) Log.v(TAG, "Update state matches expectation");
+ updatePlaybackState();
+ }
+ } else {
+ updatePlaybackState();
+ }
+ }
+
+ /**
+ * Must be called appropriately by a subclass when the metadata state has changed.
+ */
+ protected void onMetadataChanged() {
+ if (DEBUG) Log.v(TAG, "onMetadataChanged");
+ updateRowMetadata();
+ }
+}