PlaybackControlGlue.java revision d805095048f6be52cddbd572ee343c4639ba8187
1package android.support.v17.leanback.app;
2
3import android.content.Context;
4import android.graphics.drawable.Drawable;
5import android.os.Handler;
6import android.os.Message;
7import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter;
8import android.support.v17.leanback.widget.Action;
9import android.support.v17.leanback.widget.ControlButtonPresenterSelector;
10import android.support.v17.leanback.widget.OnActionClickedListener;
11import android.support.v17.leanback.widget.OnItemViewClickedListener;
12import android.support.v17.leanback.widget.PlaybackControlsRow;
13import android.support.v17.leanback.widget.PlaybackControlsRowPresenter;
14import android.support.v17.leanback.widget.Presenter;
15import android.support.v17.leanback.widget.PresenterSelector;
16import android.support.v17.leanback.widget.Row;
17import android.support.v17.leanback.widget.RowPresenter;
18import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
19import android.util.Log;
20import android.view.InputEvent;
21import android.view.KeyEvent;
22import android.view.View;
23
24
25/**
26 * A helper class for managing a {@link android.support.v17.leanback.widget.PlaybackControlsRow} and
27 * {@link PlaybackOverlayFragment} that implements a recommended approach to handling standard
28 * playback control actions such as play/pause, fast forward/rewind at progressive speed levels,
29 * and skip to next/previous.  This helper class is a glue layer in that it manages the
30 * configuration of and interaction between the leanback UI components by defining a functional
31 * interface to the media player.
32 *
33 * <p>You can instantiate a concrete subclass such as {@link MediaControllerGlue} or you must
34 * subclass this abstract helper.  To create a subclass you must implement all of the
35 * abstract methods and the subclass must invoke {@link #onMetadataChanged()} and
36 * {@link #onStateChanged()} appropriately.
37 * </p>
38 *
39 * <p>To use an instance of the glue layer, first construct an instance.  Constructor parameters
40 * inform the glue what speed levels are supported for fast forward/rewind.  Providing a
41 * {@link android.support.v17.leanback.app.PlaybackOverlayFragment} is optional.
42 * </p>
43 *
44 * <p>If you have your own controls row you must pass it to {@link #setControlsRow}.
45 * The row will be updated by the glue layer based on the media metadata and playback state.
46 * Alternatively, you may call {@link #createControlsRowAndPresenter()} which will set a controls
47 * row and return a row presenter you can use to present the row.
48 * </p>
49 *
50 * <p>The helper sets a {@link android.support.v17.leanback.widget.SparseArrayObjectAdapter}
51 * on the controls row as the primary actions adapter, and adds actions to it.  You can provide
52 * additional actions by overriding {@link #createPrimaryActionsAdapter}.  This helper does not
53 * deal in secondary actions so those you may add separately.
54 * </p>
55 *
56 * <p>Provide a click listener on your fragment and if an action is clicked, call
57 * {@link #onActionClicked}.  There is no need to call {@link #setOnItemViewClickedListener}
58 * but if you do a click listener will be installed on the fragment and recognized action clicks
59 * will be handled.  Your listener will be called only for unhandled actions.
60 * </p>
61 *
62 * <p>The helper implements a key event handler.  If you pass a
63 * {@link android.support.v17.leanback.app.PlaybackOverlayFragment} the fragment's input event
64 * handler will be set.  Otherwise, you should set the glue object as key event handler to the
65 * ViewHolder when bound by your row presenter; see
66 * {@link RowPresenter.ViewHolder#setOnKeyListener(android.view.View.OnKeyListener)}.
67 * </p>
68 *
69 * <p>To update the controls row progress during playback, override {@link #enableProgressUpdating}
70 * to manage the lifecycle of a periodic callback to {@link #updateProgress()}.
71 * {@link #getUpdatePeriod()} provides a recommended update period.
72 * </p>
73 *
74 */
75public abstract class PlaybackControlGlue implements OnActionClickedListener, View.OnKeyListener {
76    /**
77     * The adapter key for the first custom control on the left side
78     * of the predefined primary controls.
79     */
80    public static final int ACTION_CUSTOM_LEFT_FIRST = 0x1;
81
82    /**
83     * The adapter key for the skip to previous control.
84     */
85    public static final int ACTION_SKIP_TO_PREVIOUS = 0x10;
86
87    /**
88     * The adapter key for the rewind control.
89     */
90    public static final int ACTION_REWIND = 0x20;
91
92    /**
93     * The adapter key for the play/pause control.
94     */
95    public static final int ACTION_PLAY_PAUSE = 0x40;
96
97    /**
98     * The adapter key for the fast forward control.
99     */
100    public static final int ACTION_FAST_FORWARD = 0x80;
101
102    /**
103     * The adapter key for the skip to next control.
104     */
105    public static final int ACTION_SKIP_TO_NEXT = 0x100;
106
107    /**
108     * The adapter key for the first custom control on the right side
109     * of the predefined primary controls.
110     */
111    public static final int ACTION_CUSTOM_RIGHT_FIRST = 0x1000;
112
113    /**
114     * Invalid playback speed.
115     */
116    public static final int PLAYBACK_SPEED_INVALID = -1;
117
118    /**
119     * Speed representing playback state that is paused.
120     */
121    public static final int PLAYBACK_SPEED_PAUSED = 0;
122
123    /**
124     * Speed representing playback state that is playing normally.
125     */
126    public static final int PLAYBACK_SPEED_NORMAL = 1;
127
128    /**
129     * The initial (level 0) fast forward playback speed.
130     * The negative of this value is for rewind at the same speed.
131     */
132    public static final int PLAYBACK_SPEED_FAST_L0 = 10;
133
134    /**
135     * The level 1 fast forward playback speed.
136     * The negative of this value is for rewind at the same speed.
137     */
138    public static final int PLAYBACK_SPEED_FAST_L1 = 11;
139
140    /**
141     * The level 2 fast forward playback speed.
142     * The negative of this value is for rewind at the same speed.
143     */
144    public static final int PLAYBACK_SPEED_FAST_L2 = 12;
145
146    /**
147     * The level 3 fast forward playback speed.
148     * The negative of this value is for rewind at the same speed.
149     */
150    public static final int PLAYBACK_SPEED_FAST_L3 = 13;
151
152    /**
153     * The level 4 fast forward playback speed.
154     * The negative of this value is for rewind at the same speed.
155     */
156    public static final int PLAYBACK_SPEED_FAST_L4 = 14;
157
158    private static final String TAG = "PlaybackControlGlue";
159    private static final boolean DEBUG = false;
160
161    private static final int MSG_UPDATE_PLAYBACK_STATE = 100;
162    private static final int UPDATE_PLAYBACK_STATE_DELAY_MS = 2000;
163    private static final int NUMBER_OF_SEEK_SPEEDS = PLAYBACK_SPEED_FAST_L4 -
164            PLAYBACK_SPEED_FAST_L0 + 1;
165
166    private final PlaybackOverlayFragment mFragment;
167    private final Context mContext;
168    private final int[] mFastForwardSpeeds;
169    private final int[] mRewindSpeeds;
170    private PlaybackControlsRow mControlsRow;
171    private SparseArrayObjectAdapter mPrimaryActionsAdapter;
172    private PlaybackControlsRow.PlayPauseAction mPlayPauseAction;
173    private PlaybackControlsRow.SkipNextAction mSkipNextAction;
174    private PlaybackControlsRow.SkipPreviousAction mSkipPreviousAction;
175    private PlaybackControlsRow.FastForwardAction mFastForwardAction;
176    private PlaybackControlsRow.RewindAction mRewindAction;
177    private OnItemViewClickedListener mExternalOnItemViewClickedListener;
178    private int mPlaybackSpeed = PLAYBACK_SPEED_NORMAL;
179    private boolean mFadeWhenPlaying = true;
180
181    private final Handler mHandler = new Handler() {
182        @Override
183        public void handleMessage(Message msg) {
184            if (msg.what == MSG_UPDATE_PLAYBACK_STATE) {
185                updatePlaybackState();
186            }
187        }
188    };
189
190    private final OnItemViewClickedListener mOnItemViewClickedListener =
191            new OnItemViewClickedListener() {
192        @Override
193        public void onItemClicked(Presenter.ViewHolder viewHolder, Object object,
194                                  RowPresenter.ViewHolder viewHolder2, Row row) {
195            if (DEBUG) Log.v(TAG, "onItemClicked " + object);
196            boolean handled = false;
197            if (object instanceof Action) {
198                handled = dispatchAction((Action) object, null);
199            }
200            if (!handled && mExternalOnItemViewClickedListener != null) {
201                mExternalOnItemViewClickedListener.onItemClicked(viewHolder, object,
202                        viewHolder2, row);
203            }
204        }
205    };
206
207    /**
208     * Constructor for the glue.
209     *
210     * @param context
211     * @param seekSpeeds Array of seek speeds for fast forward and rewind.
212     */
213    public PlaybackControlGlue(Context context, int[] seekSpeeds) {
214        this(context, null, seekSpeeds, seekSpeeds);
215    }
216
217    /**
218     * Constructor for the glue.
219     *
220     * @param context
221     * @param fastForwardSpeeds Array of seek speeds for fast forward.
222     * @param rewindSpeeds Array of seek speeds for rewind.
223     */
224    public PlaybackControlGlue(Context context,
225                               int[] fastForwardSpeeds,
226                               int[] rewindSpeeds) {
227        this(context, null, fastForwardSpeeds, rewindSpeeds);
228    }
229
230    /**
231     * Constructor for the glue.
232     *
233     * @param context
234     * @param fragment Optional; if using a {@link PlaybackOverlayFragment}, pass it in.
235     * @param seekSpeeds Array of seek speeds for fast forward and rewind.
236     */
237    public PlaybackControlGlue(Context context,
238                               PlaybackOverlayFragment fragment,
239                               int[] seekSpeeds) {
240        this(context, fragment, seekSpeeds, seekSpeeds);
241    }
242
243    /**
244     * Constructor for the glue.
245     *
246     * @param context
247     * @param fragment Optional; if using a {@link PlaybackOverlayFragment}, pass it in.
248     * @param fastForwardSpeeds Array of seek speeds for fast forward.
249     * @param rewindSpeeds Array of seek speeds for rewind.
250     */
251    public PlaybackControlGlue(Context context,
252                               PlaybackOverlayFragment fragment,
253                               int[] fastForwardSpeeds,
254                               int[] rewindSpeeds) {
255        mContext = context;
256        mFragment = fragment;
257        if (fragment != null) {
258            attachToFragment();
259        }
260        if (fastForwardSpeeds.length == 0 || fastForwardSpeeds.length > NUMBER_OF_SEEK_SPEEDS) {
261            throw new IllegalStateException("invalid fastForwardSpeeds array size");
262        }
263        mFastForwardSpeeds = fastForwardSpeeds;
264        if (rewindSpeeds.length == 0 || rewindSpeeds.length > NUMBER_OF_SEEK_SPEEDS) {
265            throw new IllegalStateException("invalid rewindSpeeds array size");
266        }
267        mRewindSpeeds = rewindSpeeds;
268    }
269
270    private final PlaybackOverlayFragment.InputEventHandler mOnInputEventHandler =
271            new PlaybackOverlayFragment.InputEventHandler() {
272        @Override
273        public boolean handleInputEvent(InputEvent event) {
274            if (event instanceof KeyEvent) {
275                KeyEvent keyEvent = (KeyEvent) event;
276                return onKey(null, keyEvent.getKeyCode(), keyEvent);
277            }
278            return false;
279        }
280    };
281
282    private void attachToFragment() {
283        mFragment.setInputEventHandler(mOnInputEventHandler);
284    }
285
286    /**
287     * Helper method for instantiating a
288     * {@link android.support.v17.leanback.widget.PlaybackControlsRow} and corresponding
289     * {@link android.support.v17.leanback.widget.PlaybackControlsRowPresenter}.
290     */
291    public PlaybackControlsRowPresenter createControlsRowAndPresenter() {
292        PlaybackControlsRow controlsRow = new PlaybackControlsRow(this);
293        setControlsRow(controlsRow);
294
295        AbstractDetailsDescriptionPresenter detailsPresenter =
296                new AbstractDetailsDescriptionPresenter() {
297            @Override
298            protected void onBindDescription(AbstractDetailsDescriptionPresenter.ViewHolder
299                                                     viewHolder, Object object) {
300                PlaybackControlGlue glue = (PlaybackControlGlue) object;
301                if (glue.hasValidMedia()) {
302                    viewHolder.getTitle().setText(glue.getMediaTitle());
303                    viewHolder.getSubtitle().setText(glue.getMediaSubtitle());
304                } else {
305                    viewHolder.getTitle().setText("");
306                    viewHolder.getSubtitle().setText("");
307                }
308            }
309        };
310        return new PlaybackControlsRowPresenter(detailsPresenter) {
311            @Override
312            protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) {
313                super.onBindRowViewHolder(vh, item);
314                vh.setOnKeyListener(PlaybackControlGlue.this);
315            }
316            @Override
317            protected void onUnbindRowViewHolder(RowPresenter.ViewHolder vh) {
318                super.onUnbindRowViewHolder(vh);
319                vh.setOnKeyListener(null);
320            }
321        };
322    }
323
324    /**
325     * Returns the fragment.
326     */
327    public PlaybackOverlayFragment getFragment() {
328        return mFragment;
329    }
330
331    /**
332     * Returns the context.
333     */
334    public Context getContext() {
335        return mContext;
336    }
337
338    /**
339     * Returns the fast forward speeds.
340     */
341    public int[] getFastForwardSpeeds() {
342        return mFastForwardSpeeds;
343    }
344
345    /**
346     * Returns the rewind speeds.
347     */
348    public int[] getRewindSpeeds() {
349        return mRewindSpeeds;
350    }
351
352    /**
353     * Sets the controls to fade after a timeout when media is playing.
354     */
355    public void setFadingEnabled(boolean enable) {
356        mFadeWhenPlaying = enable;
357        if (!mFadeWhenPlaying && mFragment != null) {
358            mFragment.setFadingEnabled(false);
359        }
360    }
361
362    /**
363     * Returns true if controls are set to fade when media is playing.
364     */
365    public boolean isFadingEnabled() {
366        return mFadeWhenPlaying;
367    }
368
369    /**
370     * Set the {@link OnItemViewClickedListener} to be called if the click event
371     * is not handled internally.
372     * @param listener
373     * @deprecated Don't call this.  Instead set the listener on the fragment yourself,
374     * and call {@link #onActionClicked} to handle clicks.
375     */
376    @Deprecated
377    public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
378        mExternalOnItemViewClickedListener = listener;
379        if (mFragment != null) {
380            mFragment.setOnItemViewClickedListener(mOnItemViewClickedListener);
381        }
382    }
383
384    /**
385     * Returns the {@link OnItemViewClickedListener}.
386     */
387    public OnItemViewClickedListener getOnItemViewClickedListener() {
388        return mExternalOnItemViewClickedListener;
389    }
390
391    /**
392     * Sets the controls row to be managed by the glue layer.
393     * The primary actions and playback state related aspects of the row
394     * are updated by the glue.
395     */
396    public void setControlsRow(PlaybackControlsRow controlsRow) {
397        mControlsRow = controlsRow;
398        mPrimaryActionsAdapter = createPrimaryActionsAdapter(
399                new ControlButtonPresenterSelector());
400        mControlsRow.setPrimaryActionsAdapter(mPrimaryActionsAdapter);
401        updateControlsRow();
402    }
403
404    /**
405     * Returns the playback controls row managed by the glue layer.
406     */
407    public PlaybackControlsRow getControlsRow() {
408        return mControlsRow;
409    }
410
411    /**
412     * Override this to start/stop a runnable to call {@link #updateProgress} at
413     * an interval such as {@link #getUpdatePeriod}.
414     */
415    public void enableProgressUpdating(boolean enable) {
416    }
417
418    /**
419     * Returns the time period in milliseconds that should be used
420     * to update the progress.  See {@link #updateProgress()}.
421     */
422    public int getUpdatePeriod() {
423        // TODO: calculate a better update period based on total duration and screen size
424        return 500;
425    }
426
427    /**
428     * Updates the progress bar based on the current media playback position.
429     */
430    public void updateProgress() {
431        int position = getCurrentPosition();
432        if (DEBUG) Log.v(TAG, "updateProgress " + position);
433        mControlsRow.setCurrentTime(position);
434    }
435
436    /**
437     * Handles action clicks.  A subclass may override this add support for additional actions.
438     */
439    @Override
440    public void onActionClicked(Action action) {
441        dispatchAction(action, null);
442    }
443
444    /**
445     * Handles key events and returns true if handled.  A subclass may override this to provide
446     * additional support.
447     */
448    @Override
449    public boolean onKey(View v, int keyCode, KeyEvent event) {
450        switch (keyCode) {
451            case KeyEvent.KEYCODE_DPAD_UP:
452            case KeyEvent.KEYCODE_DPAD_DOWN:
453            case KeyEvent.KEYCODE_DPAD_RIGHT:
454            case KeyEvent.KEYCODE_DPAD_LEFT:
455            case KeyEvent.KEYCODE_BACK:
456            case KeyEvent.KEYCODE_ESCAPE:
457                boolean abortSeek = mPlaybackSpeed >= PLAYBACK_SPEED_FAST_L0 ||
458                        mPlaybackSpeed <= -PLAYBACK_SPEED_FAST_L0;
459                if (abortSeek) {
460                    mPlaybackSpeed = PLAYBACK_SPEED_NORMAL;
461                    startPlayback(mPlaybackSpeed);
462                    updatePlaybackStatusAfterUserAction();
463                    return keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE;
464                }
465                return false;
466        }
467        Action action = mControlsRow.getActionForKeyCode(mPrimaryActionsAdapter, keyCode);
468        if (action != null) {
469            if (action == mPrimaryActionsAdapter.lookup(ACTION_PLAY_PAUSE) ||
470                    action == mPrimaryActionsAdapter.lookup(ACTION_REWIND) ||
471                    action == mPrimaryActionsAdapter.lookup(ACTION_FAST_FORWARD) ||
472                    action == mPrimaryActionsAdapter.lookup(ACTION_SKIP_TO_PREVIOUS) ||
473                    action == mPrimaryActionsAdapter.lookup(ACTION_SKIP_TO_NEXT)) {
474                if (((KeyEvent) event).getAction() == KeyEvent.ACTION_DOWN) {
475                    dispatchAction(action, (KeyEvent) event);
476                }
477                return true;
478            }
479        }
480        return false;
481    }
482
483    /**
484     * Called when the given action is invoked, either by click or keyevent.
485     */
486    private boolean dispatchAction(Action action, KeyEvent keyEvent) {
487        boolean handled = false;
488        if (action == mPlayPauseAction) {
489            boolean canPlay = keyEvent == null ||
490                    keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE ||
491                    keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY;
492            boolean canPause = keyEvent == null ||
493                    keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE ||
494                    keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PAUSE;
495            if (mPlaybackSpeed != PLAYBACK_SPEED_NORMAL) {
496                if (canPlay) {
497                    mPlaybackSpeed = PLAYBACK_SPEED_NORMAL;
498                    startPlayback(mPlaybackSpeed);
499                }
500            } else if (canPause) {
501                mPlaybackSpeed = PLAYBACK_SPEED_PAUSED;
502                pausePlayback();
503            }
504            updatePlaybackStatusAfterUserAction();
505            handled = true;
506        } else if (action == mSkipNextAction) {
507            skipToNext();
508            handled = true;
509        } else if (action == mSkipPreviousAction) {
510            skipToPrevious();
511            handled = true;
512        } else if (action == mFastForwardAction) {
513            if (mPlaybackSpeed < getMaxForwardSpeedId()) {
514                switch (mPlaybackSpeed) {
515                    case PLAYBACK_SPEED_FAST_L0:
516                    case PLAYBACK_SPEED_FAST_L1:
517                    case PLAYBACK_SPEED_FAST_L2:
518                    case PLAYBACK_SPEED_FAST_L3:
519                        mPlaybackSpeed++;
520                        break;
521                    default:
522                        mPlaybackSpeed = PLAYBACK_SPEED_FAST_L0;
523                        break;
524                }
525                startPlayback(mPlaybackSpeed);
526                updatePlaybackStatusAfterUserAction();
527            }
528            handled = true;
529        } else if (action == mRewindAction) {
530            if (mPlaybackSpeed > -getMaxRewindSpeedId()) {
531                switch (mPlaybackSpeed) {
532                    case -PLAYBACK_SPEED_FAST_L0:
533                    case -PLAYBACK_SPEED_FAST_L1:
534                    case -PLAYBACK_SPEED_FAST_L2:
535                    case -PLAYBACK_SPEED_FAST_L3:
536                        mPlaybackSpeed--;
537                        break;
538                    default:
539                        mPlaybackSpeed = -PLAYBACK_SPEED_FAST_L0;
540                        break;
541                }
542                startPlayback(mPlaybackSpeed);
543                updatePlaybackStatusAfterUserAction();
544            }
545            handled = true;
546        }
547        return handled;
548    }
549
550    private int getMaxForwardSpeedId() {
551        return PLAYBACK_SPEED_FAST_L0 + (mFastForwardSpeeds.length - 1);
552    }
553
554    private int getMaxRewindSpeedId() {
555        return PLAYBACK_SPEED_FAST_L0 + (mRewindSpeeds.length - 1);
556    }
557
558    private void updateControlsRow() {
559        updateRowMetadata();
560        mHandler.removeMessages(MSG_UPDATE_PLAYBACK_STATE);
561        updatePlaybackState();
562    }
563
564    private void updatePlaybackStatusAfterUserAction() {
565        updatePlaybackState(mPlaybackSpeed);
566        // Sync playback state after a delay
567        mHandler.removeMessages(MSG_UPDATE_PLAYBACK_STATE);
568        mHandler.sendEmptyMessageDelayed(MSG_UPDATE_PLAYBACK_STATE,
569                UPDATE_PLAYBACK_STATE_DELAY_MS);
570    }
571
572    private void updateRowMetadata() {
573        if (mControlsRow == null) {
574            return;
575        }
576
577        if (DEBUG) Log.v(TAG, "updateRowMetadata hasValidMedia " + hasValidMedia());
578
579        if (!hasValidMedia()) {
580            mControlsRow.setImageDrawable(null);
581            mControlsRow.setTotalTime(0);
582            mControlsRow.setCurrentTime(0);
583        } else {
584            mControlsRow.setImageDrawable(getMediaArt());
585            mControlsRow.setTotalTime(getMediaDuration());
586            mControlsRow.setCurrentTime(getCurrentPosition());
587        }
588
589        onRowChanged(mControlsRow);
590    }
591
592    private void updatePlaybackState() {
593        if (hasValidMedia()) {
594            mPlaybackSpeed = getCurrentSpeedId();
595            updatePlaybackState(mPlaybackSpeed);
596        }
597    }
598
599    private void updatePlaybackState(int playbackSpeed) {
600        if (mControlsRow == null) {
601            return;
602        }
603
604        final long actions = getSupportedActions();
605        if ((actions & ACTION_SKIP_TO_PREVIOUS) != 0) {
606            if (mSkipPreviousAction == null) {
607                mSkipPreviousAction = new PlaybackControlsRow.SkipPreviousAction(mContext);
608            }
609            mPrimaryActionsAdapter.set(ACTION_SKIP_TO_PREVIOUS, mSkipPreviousAction);
610        } else {
611            mPrimaryActionsAdapter.clear(ACTION_SKIP_TO_PREVIOUS);
612            mSkipPreviousAction = null;
613        }
614        if ((actions & ACTION_REWIND) != 0) {
615            if (mRewindAction == null) {
616                mRewindAction = new PlaybackControlsRow.RewindAction(mContext,
617                        mRewindSpeeds.length);
618            }
619            mPrimaryActionsAdapter.set(ACTION_REWIND, mRewindAction);
620        } else {
621            mPrimaryActionsAdapter.clear(ACTION_REWIND);
622            mRewindAction = null;
623        }
624        if ((actions & ACTION_PLAY_PAUSE) != 0) {
625            if (mPlayPauseAction == null) {
626                mPlayPauseAction = new PlaybackControlsRow.PlayPauseAction(mContext);
627            }
628            mPrimaryActionsAdapter.set(ACTION_PLAY_PAUSE, mPlayPauseAction);
629        } else {
630            mPrimaryActionsAdapter.clear(ACTION_PLAY_PAUSE);
631            mPlayPauseAction = null;
632        }
633        if ((actions & ACTION_FAST_FORWARD) != 0) {
634            if (mFastForwardAction == null) {
635                mFastForwardAction = new PlaybackControlsRow.FastForwardAction(mContext,
636                        mFastForwardSpeeds.length);
637            }
638            mPrimaryActionsAdapter.set(ACTION_FAST_FORWARD, mFastForwardAction);
639        } else {
640            mPrimaryActionsAdapter.clear(ACTION_FAST_FORWARD);
641            mFastForwardAction = null;
642        }
643        if ((actions & ACTION_SKIP_TO_NEXT) != 0) {
644            if (mSkipNextAction == null) {
645                mSkipNextAction = new PlaybackControlsRow.SkipNextAction(mContext);
646            }
647            mPrimaryActionsAdapter.set(ACTION_SKIP_TO_NEXT, mSkipNextAction);
648        } else {
649            mPrimaryActionsAdapter.clear(ACTION_SKIP_TO_NEXT);
650            mSkipNextAction = null;
651        }
652
653        if (mFastForwardAction != null) {
654            int index = 0;
655            if (playbackSpeed >= PLAYBACK_SPEED_FAST_L0) {
656                index = playbackSpeed - PLAYBACK_SPEED_FAST_L0;
657                if (playbackSpeed < getMaxForwardSpeedId()) {
658                    index++;
659                }
660            }
661            if (mFastForwardAction.getIndex() != index) {
662                mFastForwardAction.setIndex(index);
663                notifyItemChanged(mPrimaryActionsAdapter, mFastForwardAction);
664            }
665        }
666        if (mRewindAction != null) {
667            int index = 0;
668            if (playbackSpeed <= -PLAYBACK_SPEED_FAST_L0) {
669                index = -playbackSpeed - PLAYBACK_SPEED_FAST_L0;
670                if (-playbackSpeed < getMaxRewindSpeedId()) {
671                    index++;
672                }
673            }
674            if (mRewindAction.getIndex() != index) {
675                mRewindAction.setIndex(index);
676                notifyItemChanged(mPrimaryActionsAdapter, mRewindAction);
677            }
678        }
679
680        if (playbackSpeed == PLAYBACK_SPEED_PAUSED) {
681            updateProgress();
682            enableProgressUpdating(false);
683        } else {
684            enableProgressUpdating(true);
685        }
686
687        if (mFadeWhenPlaying && mFragment != null) {
688            mFragment.setFadingEnabled(playbackSpeed == PLAYBACK_SPEED_NORMAL);
689        }
690
691        if (mPlayPauseAction != null) {
692            int index = playbackSpeed == PLAYBACK_SPEED_PAUSED ?
693                    PlaybackControlsRow.PlayPauseAction.PLAY :
694                    PlaybackControlsRow.PlayPauseAction.PAUSE;
695            if (mPlayPauseAction.getIndex() != index) {
696                mPlayPauseAction.setIndex(index);
697                notifyItemChanged(mPrimaryActionsAdapter, mPlayPauseAction);
698            }
699        }
700    }
701
702    private static void notifyItemChanged(SparseArrayObjectAdapter adapter, Object object) {
703        int index = adapter.indexOf(object);
704        if (index >= 0) {
705            adapter.notifyArrayItemRangeChanged(index, 1);
706        }
707    }
708
709    private static String getSpeedString(int speed) {
710        switch (speed) {
711            case PLAYBACK_SPEED_INVALID:
712                return "PLAYBACK_SPEED_INVALID";
713            case PLAYBACK_SPEED_PAUSED:
714                return "PLAYBACK_SPEED_PAUSED";
715            case PLAYBACK_SPEED_NORMAL:
716                return "PLAYBACK_SPEED_NORMAL";
717            case PLAYBACK_SPEED_FAST_L0:
718                return "PLAYBACK_SPEED_FAST_L0";
719            case PLAYBACK_SPEED_FAST_L1:
720                return "PLAYBACK_SPEED_FAST_L1";
721            case PLAYBACK_SPEED_FAST_L2:
722                return "PLAYBACK_SPEED_FAST_L2";
723            case PLAYBACK_SPEED_FAST_L3:
724                return "PLAYBACK_SPEED_FAST_L3";
725            case PLAYBACK_SPEED_FAST_L4:
726                return "PLAYBACK_SPEED_FAST_L4";
727            case -PLAYBACK_SPEED_FAST_L0:
728                return "-PLAYBACK_SPEED_FAST_L0";
729            case -PLAYBACK_SPEED_FAST_L1:
730                return "-PLAYBACK_SPEED_FAST_L1";
731            case -PLAYBACK_SPEED_FAST_L2:
732                return "-PLAYBACK_SPEED_FAST_L2";
733            case -PLAYBACK_SPEED_FAST_L3:
734                return "-PLAYBACK_SPEED_FAST_L3";
735            case -PLAYBACK_SPEED_FAST_L4:
736                return "-PLAYBACK_SPEED_FAST_L4";
737        }
738        return null;
739    }
740
741    /**
742     * Returns true if there is a valid media item.
743     */
744    public abstract boolean hasValidMedia();
745
746    /**
747     * Returns true if media is currently playing.
748     */
749    public abstract boolean isMediaPlaying();
750
751    /**
752     * Returns the title of the media item.
753     */
754    public abstract CharSequence getMediaTitle();
755
756    /**
757     * Returns the subtitle of the media item.
758     */
759    public abstract CharSequence getMediaSubtitle();
760
761    /**
762     * Returns the duration of the media item in milliseconds.
763     */
764    public abstract int getMediaDuration();
765
766    /**
767     * Returns a bitmap of the art for the media item.
768     */
769    public abstract Drawable getMediaArt();
770
771    /**
772     * Returns a bitmask of actions supported by the media player.
773     */
774    public abstract long getSupportedActions();
775
776    /**
777     * Returns the current playback speed.  When playing normally,
778     * {@link #PLAYBACK_SPEED_NORMAL} should be returned.
779     */
780    public abstract int getCurrentSpeedId();
781
782    /**
783     * Returns the current position of the media item in milliseconds.
784     */
785    public abstract int getCurrentPosition();
786
787    /**
788     * Start playback at the given speed.
789     * @param speed The desired playback speed.  For normal playback this will be
790     *              {@link #PLAYBACK_SPEED_NORMAL}; higher positive values for fast forward,
791     *              and negative values for rewind.
792     */
793    protected abstract void startPlayback(int speed);
794
795    /**
796     * Pause playback.
797     */
798    protected abstract void pausePlayback();
799
800    /**
801     * Skip to the next track.
802     */
803    protected abstract void skipToNext();
804
805    /**
806     * Skip to the previous track.
807     */
808    protected abstract void skipToPrevious();
809
810    /**
811     * Invoked when the playback controls row has changed.  The adapter containing this row
812     * should be notified.
813     */
814    protected abstract void onRowChanged(PlaybackControlsRow row);
815
816    /**
817     * Creates the primary action adapter.  May be overridden to add additional primary
818     * actions to the adapter.
819     */
820    protected SparseArrayObjectAdapter createPrimaryActionsAdapter(
821            PresenterSelector presenterSelector) {
822        return new SparseArrayObjectAdapter(presenterSelector);
823    }
824
825    /**
826     * Must be called appropriately by a subclass when the playback state has changed.
827     */
828    protected void onStateChanged() {
829        if (DEBUG) Log.v(TAG, "onStateChanged");
830        // If a pending control button update is present, delay
831        // the update until the state settles.
832        if (!hasValidMedia()) {
833            return;
834        }
835        if (mHandler.hasMessages(MSG_UPDATE_PLAYBACK_STATE)) {
836            mHandler.removeMessages(MSG_UPDATE_PLAYBACK_STATE);
837            if (getCurrentSpeedId() != mPlaybackSpeed) {
838                if (DEBUG) Log.v(TAG, "Status expectation mismatch, delaying update");
839                mHandler.sendEmptyMessageDelayed(MSG_UPDATE_PLAYBACK_STATE,
840                        UPDATE_PLAYBACK_STATE_DELAY_MS);
841            } else {
842                if (DEBUG) Log.v(TAG, "Update state matches expectation");
843                updatePlaybackState();
844            }
845        } else {
846            updatePlaybackState();
847        }
848    }
849
850    /**
851     * Must be called appropriately by a subclass when the metadata state has changed.
852     */
853    protected void onMetadataChanged() {
854        if (DEBUG) Log.v(TAG, "onMetadataChanged");
855        updateRowMetadata();
856    }
857}
858