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