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 PlaybackOverlayFragment} 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.PlaybackOverlayFragment} 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.PlaybackOverlayFragment} 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 PlaybackControlGlue 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 = "PlaybackControlGlue"; 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 PlaybackOverlayFragment 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 PlaybackControlGlue(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 PlaybackControlGlue(Context context, int[] fastForwardSpeeds, int[] rewindSpeeds) { this(context, null, fastForwardSpeeds, rewindSpeeds); } /** * Constructor for the glue. * * @param context * @param fragment Optional; if using a {@link PlaybackOverlayFragment}, pass it in. * @param seekSpeeds Array of seek speeds for fast forward and rewind. */ public PlaybackControlGlue(Context context, PlaybackOverlayFragment fragment, int[] seekSpeeds) { this(context, fragment, seekSpeeds, seekSpeeds); } /** * Constructor for the glue. * * @param context * @param fragment Optional; if using a {@link PlaybackOverlayFragment}, pass it in. * @param fastForwardSpeeds Array of seek speeds for fast forward. * @param rewindSpeeds Array of seek speeds for rewind. */ public PlaybackControlGlue(Context context, PlaybackOverlayFragment 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 PlaybackOverlayFragment.InputEventHandler mOnInputEventHandler = new PlaybackOverlayFragment.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) { PlaybackControlGlue glue = (PlaybackControlGlue) 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(PlaybackControlGlue.this); } @Override protected void onUnbindRowViewHolder(RowPresenter.ViewHolder vh) { super.onUnbindRowViewHolder(vh); vh.setOnKeyListener(null); } }; } /** * Returns the fragment. */ public PlaybackOverlayFragment 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(); } }