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