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 right 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    public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
377        mExternalOnItemViewClickedListener = listener;
378        if (mFragment != null) {
379            mFragment.setOnItemViewClickedListener(mOnItemViewClickedListener);
380        }
381    }
382
383    /**
384     * Returns the {@link OnItemViewClickedListener}.
385     */
386    public OnItemViewClickedListener getOnItemViewClickedListener() {
387        return mExternalOnItemViewClickedListener;
388    }
389
390    /**
391     * Sets the controls row to be managed by the glue layer.
392     * The primary actions and playback state related aspects of the row
393     * are updated by the glue.
394     */
395    public void setControlsRow(PlaybackControlsRow controlsRow) {
396        mControlsRow = controlsRow;
397        mPrimaryActionsAdapter = createPrimaryActionsAdapter(
398                new ControlButtonPresenterSelector());
399        mControlsRow.setPrimaryActionsAdapter(mPrimaryActionsAdapter);
400        updateControlsRow();
401    }
402
403    /**
404     * Returns the playback controls row managed by the glue layer.
405     */
406    public PlaybackControlsRow getControlsRow() {
407        return mControlsRow;
408    }
409
410    /**
411     * Override this to start/stop a runnable to call {@link #updateProgress} at
412     * an interval such as {@link #getUpdatePeriod}.
413     */
414    public void enableProgressUpdating(boolean enable) {
415    }
416
417    /**
418     * Returns the time period in milliseconds that should be used
419     * to update the progress.  See {@link #updateProgress()}.
420     */
421    public int getUpdatePeriod() {
422        // TODO: calculate a better update period based on total duration and screen size
423        return 500;
424    }
425
426    /**
427     * Updates the progress bar based on the current media playback position.
428     */
429    public void updateProgress() {
430        int position = getCurrentPosition();
431        if (DEBUG) Log.v(TAG, "updateProgress " + position);
432        mControlsRow.setCurrentTime(position);
433    }
434
435    /**
436     * Handles action clicks.  A subclass may override this add support for additional actions.
437     */
438    @Override
439    public void onActionClicked(Action action) {
440        dispatchAction(action, null);
441    }
442
443    /**
444     * Handles key events and returns true if handled.  A subclass may override this to provide
445     * additional support.
446     */
447    @Override
448    public boolean onKey(View v, int keyCode, KeyEvent event) {
449        switch (keyCode) {
450            case KeyEvent.KEYCODE_DPAD_UP:
451            case KeyEvent.KEYCODE_DPAD_DOWN:
452            case KeyEvent.KEYCODE_DPAD_RIGHT:
453            case KeyEvent.KEYCODE_DPAD_LEFT:
454            case KeyEvent.KEYCODE_BACK:
455            case KeyEvent.KEYCODE_ESCAPE:
456                boolean abortSeek = mPlaybackSpeed >= PLAYBACK_SPEED_FAST_L0 ||
457                        mPlaybackSpeed <= -PLAYBACK_SPEED_FAST_L0;
458                if (abortSeek) {
459                    mPlaybackSpeed = PLAYBACK_SPEED_NORMAL;
460                    startPlayback(mPlaybackSpeed);
461                    updatePlaybackStatusAfterUserAction();
462                    return keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE;
463                }
464                return false;
465        }
466        Action action = mControlsRow.getActionForKeyCode(mPrimaryActionsAdapter, keyCode);
467        if (action != null) {
468            if (action == mPrimaryActionsAdapter.lookup(ACTION_PLAY_PAUSE) ||
469                    action == mPrimaryActionsAdapter.lookup(ACTION_REWIND) ||
470                    action == mPrimaryActionsAdapter.lookup(ACTION_FAST_FORWARD) ||
471                    action == mPrimaryActionsAdapter.lookup(ACTION_SKIP_TO_PREVIOUS) ||
472                    action == mPrimaryActionsAdapter.lookup(ACTION_SKIP_TO_NEXT)) {
473                if (((KeyEvent) event).getAction() == KeyEvent.ACTION_DOWN) {
474                    dispatchAction(action, (KeyEvent) event);
475                }
476                return true;
477            }
478        }
479        return false;
480    }
481
482    /**
483     * Called when the given action is invoked, either by click or keyevent.
484     */
485    private boolean dispatchAction(Action action, KeyEvent keyEvent) {
486        boolean handled = false;
487        if (action == mPlayPauseAction) {
488            boolean canPlay = keyEvent == null ||
489                    keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE ||
490                    keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY;
491            boolean canPause = keyEvent == null ||
492                    keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE ||
493                    keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PAUSE;
494            if (mPlaybackSpeed != PLAYBACK_SPEED_NORMAL) {
495                if (canPlay) {
496                    mPlaybackSpeed = PLAYBACK_SPEED_NORMAL;
497                    startPlayback(mPlaybackSpeed);
498                }
499            } else if (canPause) {
500                mPlaybackSpeed = PLAYBACK_SPEED_PAUSED;
501                pausePlayback();
502            }
503            updatePlaybackStatusAfterUserAction();
504            handled = true;
505        } else if (action == mSkipNextAction) {
506            skipToNext();
507            handled = true;
508        } else if (action == mSkipPreviousAction) {
509            skipToPrevious();
510            handled = true;
511        } else if (action == mFastForwardAction) {
512            if (mPlaybackSpeed < getMaxForwardSpeedId()) {
513                switch (mPlaybackSpeed) {
514                    case PLAYBACK_SPEED_FAST_L0:
515                    case PLAYBACK_SPEED_FAST_L1:
516                    case PLAYBACK_SPEED_FAST_L2:
517                    case PLAYBACK_SPEED_FAST_L3:
518                        mPlaybackSpeed++;
519                        break;
520                    default:
521                        mPlaybackSpeed = PLAYBACK_SPEED_FAST_L0;
522                        break;
523                }
524                startPlayback(mPlaybackSpeed);
525                updatePlaybackStatusAfterUserAction();
526            }
527            handled = true;
528        } else if (action == mRewindAction) {
529            if (mPlaybackSpeed > -getMaxRewindSpeedId()) {
530                switch (mPlaybackSpeed) {
531                    case -PLAYBACK_SPEED_FAST_L0:
532                    case -PLAYBACK_SPEED_FAST_L1:
533                    case -PLAYBACK_SPEED_FAST_L2:
534                    case -PLAYBACK_SPEED_FAST_L3:
535                        mPlaybackSpeed--;
536                        break;
537                    default:
538                        mPlaybackSpeed = -PLAYBACK_SPEED_FAST_L0;
539                        break;
540                }
541                startPlayback(mPlaybackSpeed);
542                updatePlaybackStatusAfterUserAction();
543            }
544            handled = true;
545        }
546        return handled;
547    }
548
549    private int getMaxForwardSpeedId() {
550        return PLAYBACK_SPEED_FAST_L0 + (mFastForwardSpeeds.length - 1);
551    }
552
553    private int getMaxRewindSpeedId() {
554        return PLAYBACK_SPEED_FAST_L0 + (mRewindSpeeds.length - 1);
555    }
556
557    private void updateControlsRow() {
558        updateRowMetadata();
559        mHandler.removeMessages(MSG_UPDATE_PLAYBACK_STATE);
560        updatePlaybackState();
561    }
562
563    private void updatePlaybackStatusAfterUserAction() {
564        updatePlaybackState(mPlaybackSpeed);
565        // Sync playback state after a delay
566        mHandler.removeMessages(MSG_UPDATE_PLAYBACK_STATE);
567        mHandler.sendEmptyMessageDelayed(MSG_UPDATE_PLAYBACK_STATE,
568                UPDATE_PLAYBACK_STATE_DELAY_MS);
569    }
570
571    private void updateRowMetadata() {
572        if (mControlsRow == null) {
573            return;
574        }
575
576        if (DEBUG) Log.v(TAG, "updateRowMetadata hasValidMedia " + hasValidMedia());
577
578        if (!hasValidMedia()) {
579            mControlsRow.setImageDrawable(null);
580            mControlsRow.setTotalTime(0);
581            mControlsRow.setCurrentTime(0);
582        } else {
583            mControlsRow.setImageDrawable(getMediaArt());
584            mControlsRow.setTotalTime(getMediaDuration());
585            mControlsRow.setCurrentTime(getCurrentPosition());
586        }
587
588        onRowChanged(mControlsRow);
589    }
590
591    private void updatePlaybackState() {
592        if (hasValidMedia()) {
593            mPlaybackSpeed = getCurrentSpeedId();
594            updatePlaybackState(mPlaybackSpeed);
595        }
596    }
597
598    private void updatePlaybackState(int playbackSpeed) {
599        if (mControlsRow == null) {
600            return;
601        }
602
603        final long actions = getSupportedActions();
604        if ((actions & ACTION_SKIP_TO_PREVIOUS) != 0) {
605            if (mSkipPreviousAction == null) {
606                mSkipPreviousAction = new PlaybackControlsRow.SkipPreviousAction(mContext);
607            }
608            mPrimaryActionsAdapter.set(ACTION_SKIP_TO_PREVIOUS, mSkipPreviousAction);
609        } else {
610            mPrimaryActionsAdapter.clear(ACTION_SKIP_TO_PREVIOUS);
611            mSkipPreviousAction = null;
612        }
613        if ((actions & ACTION_REWIND) != 0) {
614            if (mRewindAction == null) {
615                mRewindAction = new PlaybackControlsRow.RewindAction(mContext,
616                        mRewindSpeeds.length);
617            }
618            mPrimaryActionsAdapter.set(ACTION_REWIND, mRewindAction);
619        } else {
620            mPrimaryActionsAdapter.clear(ACTION_REWIND);
621            mRewindAction = null;
622        }
623        if ((actions & ACTION_PLAY_PAUSE) != 0) {
624            if (mPlayPauseAction == null) {
625                mPlayPauseAction = new PlaybackControlsRow.PlayPauseAction(mContext);
626            }
627            mPrimaryActionsAdapter.set(ACTION_PLAY_PAUSE, mPlayPauseAction);
628        } else {
629            mPrimaryActionsAdapter.clear(ACTION_PLAY_PAUSE);
630            mPlayPauseAction = null;
631        }
632        if ((actions & ACTION_FAST_FORWARD) != 0) {
633            if (mFastForwardAction == null) {
634                mFastForwardAction = new PlaybackControlsRow.FastForwardAction(mContext,
635                        mFastForwardSpeeds.length);
636            }
637            mPrimaryActionsAdapter.set(ACTION_FAST_FORWARD, mFastForwardAction);
638        } else {
639            mPrimaryActionsAdapter.clear(ACTION_FAST_FORWARD);
640            mFastForwardAction = null;
641        }
642        if ((actions & ACTION_SKIP_TO_NEXT) != 0) {
643            if (mSkipNextAction == null) {
644                mSkipNextAction = new PlaybackControlsRow.SkipNextAction(mContext);
645            }
646            mPrimaryActionsAdapter.set(ACTION_SKIP_TO_NEXT, mSkipNextAction);
647        } else {
648            mPrimaryActionsAdapter.clear(ACTION_SKIP_TO_NEXT);
649            mSkipNextAction = null;
650        }
651
652        if (mFastForwardAction != null) {
653            int index = 0;
654            if (playbackSpeed >= PLAYBACK_SPEED_FAST_L0) {
655                index = playbackSpeed - PLAYBACK_SPEED_FAST_L0;
656                if (playbackSpeed < getMaxForwardSpeedId()) {
657                    index++;
658                }
659            }
660            if (mFastForwardAction.getIndex() != index) {
661                mFastForwardAction.setIndex(index);
662                notifyItemChanged(mPrimaryActionsAdapter, mFastForwardAction);
663            }
664        }
665        if (mRewindAction != null) {
666            int index = 0;
667            if (playbackSpeed <= -PLAYBACK_SPEED_FAST_L0) {
668                index = -playbackSpeed - PLAYBACK_SPEED_FAST_L0;
669                if (-playbackSpeed < getMaxRewindSpeedId()) {
670                    index++;
671                }
672            }
673            if (mRewindAction.getIndex() != index) {
674                mRewindAction.setIndex(index);
675                notifyItemChanged(mPrimaryActionsAdapter, mRewindAction);
676            }
677        }
678
679        if (playbackSpeed == PLAYBACK_SPEED_PAUSED) {
680            updateProgress();
681            enableProgressUpdating(false);
682        } else {
683            enableProgressUpdating(true);
684        }
685
686        if (mFadeWhenPlaying && mFragment != null) {
687            mFragment.setFadingEnabled(playbackSpeed == PLAYBACK_SPEED_NORMAL);
688        }
689
690        if (mPlayPauseAction != null) {
691            int index = playbackSpeed == PLAYBACK_SPEED_PAUSED ?
692                    PlaybackControlsRow.PlayPauseAction.PLAY :
693                    PlaybackControlsRow.PlayPauseAction.PAUSE;
694            if (mPlayPauseAction.getIndex() != index) {
695                mPlayPauseAction.setIndex(index);
696                notifyItemChanged(mPrimaryActionsAdapter, mPlayPauseAction);
697            }
698        }
699    }
700
701    private static void notifyItemChanged(SparseArrayObjectAdapter adapter, Object object) {
702        int index = adapter.indexOf(object);
703        if (index >= 0) {
704            adapter.notifyArrayItemRangeChanged(index, 1);
705        }
706    }
707
708    private static String getSpeedString(int speed) {
709        switch (speed) {
710            case PLAYBACK_SPEED_INVALID:
711                return "PLAYBACK_SPEED_INVALID";
712            case PLAYBACK_SPEED_PAUSED:
713                return "PLAYBACK_SPEED_PAUSED";
714            case PLAYBACK_SPEED_NORMAL:
715                return "PLAYBACK_SPEED_NORMAL";
716            case PLAYBACK_SPEED_FAST_L0:
717                return "PLAYBACK_SPEED_FAST_L0";
718            case PLAYBACK_SPEED_FAST_L1:
719                return "PLAYBACK_SPEED_FAST_L1";
720            case PLAYBACK_SPEED_FAST_L2:
721                return "PLAYBACK_SPEED_FAST_L2";
722            case PLAYBACK_SPEED_FAST_L3:
723                return "PLAYBACK_SPEED_FAST_L3";
724            case PLAYBACK_SPEED_FAST_L4:
725                return "PLAYBACK_SPEED_FAST_L4";
726            case -PLAYBACK_SPEED_FAST_L0:
727                return "-PLAYBACK_SPEED_FAST_L0";
728            case -PLAYBACK_SPEED_FAST_L1:
729                return "-PLAYBACK_SPEED_FAST_L1";
730            case -PLAYBACK_SPEED_FAST_L2:
731                return "-PLAYBACK_SPEED_FAST_L2";
732            case -PLAYBACK_SPEED_FAST_L3:
733                return "-PLAYBACK_SPEED_FAST_L3";
734            case -PLAYBACK_SPEED_FAST_L4:
735                return "-PLAYBACK_SPEED_FAST_L4";
736        }
737        return null;
738    }
739
740    /**
741     * Returns true if there is a valid media item.
742     */
743    public abstract boolean hasValidMedia();
744
745    /**
746     * Returns true if media is currently playing.
747     */
748    public abstract boolean isMediaPlaying();
749
750    /**
751     * Returns the title of the media item.
752     */
753    public abstract CharSequence getMediaTitle();
754
755    /**
756     * Returns the subtitle of the media item.
757     */
758    public abstract CharSequence getMediaSubtitle();
759
760    /**
761     * Returns the duration of the media item in milliseconds.
762     */
763    public abstract int getMediaDuration();
764
765    /**
766     * Returns a bitmap of the art for the media item.
767     */
768    public abstract Drawable getMediaArt();
769
770    /**
771     * Returns a bitmask of actions supported by the media player.
772     */
773    public abstract long getSupportedActions();
774
775    /**
776     * Returns the current playback speed.  When playing normally,
777     * {@link #PLAYBACK_SPEED_NORMAL} should be returned.
778     */
779    public abstract int getCurrentSpeedId();
780
781    /**
782     * Returns the current position of the media item in milliseconds.
783     */
784    public abstract int getCurrentPosition();
785
786    /**
787     * Start playback at the given speed.
788     * @param speed The desired playback speed.  For normal playback this will be
789     *              {@link #PLAYBACK_SPEED_NORMAL}; higher positive values for fast forward,
790     *              and negative values for rewind.
791     */
792    protected abstract void startPlayback(int speed);
793
794    /**
795     * Pause playback.
796     */
797    protected abstract void pausePlayback();
798
799    /**
800     * Skip to the next track.
801     */
802    protected abstract void skipToNext();
803
804    /**
805     * Skip to the previous track.
806     */
807    protected abstract void skipToPrevious();
808
809    /**
810     * Invoked when the playback controls row has changed.  The adapter containing this row
811     * should be notified.
812     */
813    protected abstract void onRowChanged(PlaybackControlsRow row);
814
815    /**
816     * Creates the primary action adapter.  May be overridden to add additional primary
817     * actions to the adapter.
818     */
819    protected SparseArrayObjectAdapter createPrimaryActionsAdapter(
820            PresenterSelector presenterSelector) {
821        return new SparseArrayObjectAdapter(presenterSelector);
822    }
823
824    /**
825     * Must be called appropriately by a subclass when the playback state has changed.
826     */
827    protected void onStateChanged() {
828        if (DEBUG) Log.v(TAG, "onStateChanged");
829        // If a pending control button update is present, delay
830        // the update until the state settles.
831        if (!hasValidMedia()) {
832            return;
833        }
834        if (mHandler.hasMessages(MSG_UPDATE_PLAYBACK_STATE)) {
835            mHandler.removeMessages(MSG_UPDATE_PLAYBACK_STATE);
836            if (getCurrentSpeedId() != mPlaybackSpeed) {
837                if (DEBUG) Log.v(TAG, "Status expectation mismatch, delaying update");
838                mHandler.sendEmptyMessageDelayed(MSG_UPDATE_PLAYBACK_STATE,
839                        UPDATE_PLAYBACK_STATE_DELAY_MS);
840            } else {
841                if (DEBUG) Log.v(TAG, "Update state matches expectation");
842                updatePlaybackState();
843            }
844        } else {
845            updatePlaybackState();
846        }
847    }
848
849    /**
850     * Must be called appropriately by a subclass when the metadata state has changed.
851     */
852    protected void onMetadataChanged() {
853        if (DEBUG) Log.v(TAG, "onMetadataChanged");
854        updateRowMetadata();
855    }
856}
857