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