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