1 package android.support.v17.leanback.app;
2 
3 import android.content.Context;
4 import android.graphics.drawable.Drawable;
5 import android.os.Handler;
6 import android.os.Message;
7 import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter;
8 import android.support.v17.leanback.widget.Action;
9 import android.support.v17.leanback.widget.ControlButtonPresenterSelector;
10 import android.support.v17.leanback.widget.OnActionClickedListener;
11 import android.support.v17.leanback.widget.OnItemViewClickedListener;
12 import android.support.v17.leanback.widget.PlaybackControlsRow;
13 import android.support.v17.leanback.widget.PlaybackControlsRowPresenter;
14 import android.support.v17.leanback.widget.Presenter;
15 import android.support.v17.leanback.widget.PresenterSelector;
16 import android.support.v17.leanback.widget.Row;
17 import android.support.v17.leanback.widget.RowPresenter;
18 import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
19 import android.util.Log;
20 import android.view.InputEvent;
21 import android.view.KeyEvent;
22 import 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  */
75 public 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      */
PlaybackControlGlue(Context context, int[] seekSpeeds)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      */
PlaybackControlGlue(Context context, int[] fastForwardSpeeds, int[] rewindSpeeds)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      */
PlaybackControlGlue(Context context, PlaybackOverlayFragment fragment, int[] seekSpeeds)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      */
PlaybackControlGlue(Context context, PlaybackOverlayFragment fragment, int[] fastForwardSpeeds, int[] rewindSpeeds)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 
attachToFragment()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      */
createControlsRowAndPresenter()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