1 /* 2 * Copyright 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.car.media.common.playback; 18 19 import static androidx.lifecycle.Transformations.switchMap; 20 21 import static com.android.car.arch.common.LiveDataFunctions.dataOf; 22 import static com.android.car.media.common.playback.PlaybackStateAnnotations.Actions; 23 24 import android.annotation.IntDef; 25 import android.annotation.NonNull; 26 import android.annotation.Nullable; 27 import android.app.Application; 28 import android.content.Context; 29 import android.content.pm.PackageManager; 30 import android.content.res.Resources; 31 import android.graphics.drawable.Drawable; 32 import android.media.MediaMetadata; 33 import android.os.Bundle; 34 import android.support.v4.media.MediaMetadataCompat; 35 import android.support.v4.media.RatingCompat; 36 import android.support.v4.media.session.MediaControllerCompat; 37 import android.support.v4.media.session.MediaSessionCompat; 38 import android.support.v4.media.session.PlaybackStateCompat; 39 import android.util.Log; 40 41 import androidx.lifecycle.AndroidViewModel; 42 import androidx.lifecycle.LiveData; 43 import androidx.lifecycle.MutableLiveData; 44 import androidx.lifecycle.Observer; 45 46 import com.android.car.media.common.CustomPlaybackAction; 47 import com.android.car.media.common.MediaConstants; 48 import com.android.car.media.common.MediaItemMetadata; 49 import com.android.car.media.common.R; 50 import com.android.car.media.common.source.MediaSourceColors; 51 import com.android.car.media.common.source.MediaSourceViewModel; 52 import com.android.internal.annotations.VisibleForTesting; 53 54 import java.lang.annotation.Retention; 55 import java.lang.annotation.RetentionPolicy; 56 import java.util.ArrayList; 57 import java.util.Collections; 58 import java.util.List; 59 import java.util.Objects; 60 import java.util.stream.Collectors; 61 62 /** 63 * ViewModel for media playback. 64 * <p> 65 * Observes changes to the provided MediaController to expose playback state and metadata 66 * observables. 67 * <p> 68 * PlaybackViewModel is a singleton tied to the application to provide a single source of truth. 69 */ 70 public class PlaybackViewModel extends AndroidViewModel { 71 private static final String TAG = "PlaybackViewModel"; 72 73 private static final String ACTION_SET_RATING = 74 "com.android.car.media.common.ACTION_SET_RATING"; 75 private static final String EXTRA_SET_HEART = "com.android.car.media.common.EXTRA_SET_HEART"; 76 77 private static PlaybackViewModel sInstance; 78 79 /** Returns the PlaybackViewModel singleton tied to the application. */ get(@onNull Application application)80 public static PlaybackViewModel get(@NonNull Application application) { 81 if (sInstance == null) { 82 sInstance = new PlaybackViewModel(application); 83 } 84 return sInstance; 85 } 86 87 /** 88 * Possible main actions. 89 */ 90 @IntDef({ACTION_PLAY, ACTION_STOP, ACTION_PAUSE, ACTION_DISABLED}) 91 @Retention(RetentionPolicy.SOURCE) 92 public @interface Action { 93 } 94 95 /** 96 * Main action is disabled. The source can't play media at this time 97 */ 98 public static final int ACTION_DISABLED = 0; 99 /** 100 * Start playing 101 */ 102 public static final int ACTION_PLAY = 1; 103 /** 104 * Stop playing 105 */ 106 public static final int ACTION_STOP = 2; 107 /** 108 * Pause playing 109 */ 110 public static final int ACTION_PAUSE = 3; 111 112 /** Needs to be a MediaMetadata because the compat class doesn't implement equals... */ 113 private static final MediaMetadata EMPTY_MEDIA_METADATA = new MediaMetadata.Builder().build(); 114 115 private final MediaControllerCallback mMediaControllerCallback = new MediaControllerCallback(); 116 private final Observer<MediaControllerCompat> mMediaControllerObserver = 117 mMediaControllerCallback::onMediaControllerChanged; 118 119 private final MediaSourceColors.Factory mColorsFactory; 120 private final MutableLiveData<MediaSourceColors> mColors = dataOf(null); 121 122 private final MutableLiveData<MediaItemMetadata> mMetadata = dataOf(null); 123 124 // Filters out queue items with no description or title and converts them to MediaItemMetadata 125 private final MutableLiveData<List<MediaItemMetadata>> mSanitizedQueue = dataOf(null); 126 127 private final MutableLiveData<Boolean> mHasQueue = dataOf(null); 128 129 private final MutableLiveData<CharSequence> mQueueTitle = dataOf(null); 130 131 private final MutableLiveData<PlaybackController> mPlaybackControls = dataOf(null); 132 133 private final MutableLiveData<PlaybackStateWrapper> mPlaybackStateWrapper = dataOf(null); 134 135 private final LiveData<PlaybackProgress> mProgress = 136 switchMap(mPlaybackStateWrapper, 137 state -> state == null ? dataOf(new PlaybackProgress(0L, 0L)) 138 : new ProgressLiveData(state.mState, state.getMaxProgress())); 139 PlaybackViewModel(Application application)140 private PlaybackViewModel(Application application) { 141 this(application, MediaSourceViewModel.get(application).getMediaController()); 142 } 143 144 @VisibleForTesting PlaybackViewModel(Application application, LiveData<MediaControllerCompat> controller)145 public PlaybackViewModel(Application application, LiveData<MediaControllerCompat> controller) { 146 super(application); 147 mColorsFactory = new MediaSourceColors.Factory(application); 148 controller.observeForever(mMediaControllerObserver); 149 } 150 151 /** 152 * Returns a LiveData that emits the colors for the currently set media source. 153 */ getMediaSourceColors()154 public LiveData<MediaSourceColors> getMediaSourceColors() { 155 return mColors; 156 } 157 158 /** 159 * Returns a LiveData that emits a MediaItemMetadata of the current media item in the session 160 * managed by the provided {@link MediaControllerCompat}. 161 */ getMetadata()162 public LiveData<MediaItemMetadata> getMetadata() { 163 return mMetadata; 164 } 165 166 /** 167 * Returns a LiveData that emits the current queue as MediaItemMetadatas where items without a 168 * title have been filtered out. 169 */ getQueue()170 public LiveData<List<MediaItemMetadata>> getQueue() { 171 return mSanitizedQueue; 172 } 173 174 /** 175 * Returns a LiveData that emits whether the MediaController has a non-empty queue 176 */ hasQueue()177 public LiveData<Boolean> hasQueue() { 178 return mHasQueue; 179 } 180 181 /** 182 * Returns a LiveData that emits the current queue title. 183 */ getQueueTitle()184 public LiveData<CharSequence> getQueueTitle() { 185 return mQueueTitle; 186 } 187 188 /** 189 * Returns a LiveData that emits an object for controlling the currently selected 190 * MediaController. 191 */ getPlaybackController()192 public LiveData<PlaybackController> getPlaybackController() { 193 return mPlaybackControls; 194 } 195 196 /** Returns a {@PlaybackStateWrapper} live data. */ getPlaybackStateWrapper()197 public LiveData<PlaybackStateWrapper> getPlaybackStateWrapper() { 198 return mPlaybackStateWrapper; 199 } 200 201 /** 202 * Returns a LiveData that emits the current playback progress, in milliseconds. This is a 203 * value between 0 and {@link #getPlaybackStateWrapper#getMaxProgress()} or 204 * {@link PlaybackStateCompat#PLAYBACK_POSITION_UNKNOWN} if the current position is unknown. 205 * This value will update on its own periodically (less than a second) while active. 206 */ getProgress()207 public LiveData<PlaybackProgress> getProgress() { 208 return mProgress; 209 } 210 211 @VisibleForTesting getMediaController()212 MediaControllerCompat getMediaController() { 213 return mMediaControllerCallback.mMediaController; 214 } 215 216 @VisibleForTesting getMediaMetadata()217 MediaMetadataCompat getMediaMetadata() { 218 return mMediaControllerCallback.mMediaMetadata; 219 } 220 221 222 private class MediaControllerCallback extends MediaControllerCompat.Callback { 223 224 private MediaControllerCompat mMediaController; 225 private MediaMetadataCompat mMediaMetadata; 226 private PlaybackStateCompat mPlaybackState; 227 onMediaControllerChanged(MediaControllerCompat controller)228 void onMediaControllerChanged(MediaControllerCompat controller) { 229 if (mMediaController == controller) { 230 Log.w(TAG, "onMediaControllerChanged noop"); 231 return; 232 } 233 234 if (mMediaController != null) { 235 mMediaController.unregisterCallback(this); 236 } 237 238 mMediaMetadata = null; 239 mPlaybackState = null; 240 mMediaController = controller; 241 mPlaybackControls.setValue(new PlaybackController(controller)); 242 243 if (mMediaController != null) { 244 mMediaController.registerCallback(this); 245 246 mColors.setValue(mColorsFactory.extractColors(controller.getPackageName())); 247 248 // The apps don't always send updates so make sure we fetch the most recent values. 249 onMetadataChanged(mMediaController.getMetadata()); 250 onPlaybackStateChanged(mMediaController.getPlaybackState()); 251 onQueueChanged(mMediaController.getQueue()); 252 onQueueTitleChanged(mMediaController.getQueueTitle()); 253 } else { 254 mColors.setValue(null); 255 onMetadataChanged(null); 256 onPlaybackStateChanged(null); 257 onQueueChanged(null); 258 onQueueTitleChanged(null); 259 } 260 261 updatePlaybackStatus(); 262 } 263 264 @Override onSessionDestroyed()265 public void onSessionDestroyed() { 266 Log.w(TAG, "onSessionDestroyed"); 267 onMediaControllerChanged(null); 268 } 269 270 @Override onMetadataChanged(@ullable MediaMetadataCompat mmdCompat)271 public void onMetadataChanged(@Nullable MediaMetadataCompat mmdCompat) { 272 // MediaSession#setMetadata builds an empty MediaMetadata when its argument is null, 273 // yet MediaMetadataCompat doesn't implement equals... so if the given mmdCompat's 274 // MediaMetadata equals EMPTY_MEDIA_METADATA, set mMediaMetadata to null to keep 275 // the code simpler everywhere else. 276 if ((mmdCompat != null) && EMPTY_MEDIA_METADATA.equals(mmdCompat.getMediaMetadata())) { 277 mMediaMetadata = null; 278 } else { 279 mMediaMetadata = mmdCompat; 280 } 281 MediaItemMetadata item = 282 (mMediaMetadata != null) ? new MediaItemMetadata(mMediaMetadata) : null; 283 mMetadata.setValue(item); 284 updatePlaybackStatus(); 285 } 286 287 @Override onQueueTitleChanged(CharSequence title)288 public void onQueueTitleChanged(CharSequence title) { 289 mQueueTitle.setValue(title); 290 } 291 292 @Override onQueueChanged(@ullable List<MediaSessionCompat.QueueItem> queue)293 public void onQueueChanged(@Nullable List<MediaSessionCompat.QueueItem> queue) { 294 List<MediaItemMetadata> filtered = queue == null ? Collections.emptyList() 295 : queue.stream() 296 .filter(item -> item.getDescription() != null 297 && item.getDescription().getTitle() != null) 298 .map(MediaItemMetadata::new) 299 .collect(Collectors.toList()); 300 301 mSanitizedQueue.setValue(filtered); 302 mHasQueue.setValue(filtered.size() > 1); 303 } 304 305 @Override onPlaybackStateChanged(PlaybackStateCompat playbackState)306 public void onPlaybackStateChanged(PlaybackStateCompat playbackState) { 307 mPlaybackState = playbackState; 308 updatePlaybackStatus(); 309 } 310 updatePlaybackStatus()311 private void updatePlaybackStatus() { 312 if (mMediaController != null && mPlaybackState != null) { 313 mPlaybackStateWrapper.setValue( 314 new PlaybackStateWrapper(mMediaController, mMediaMetadata, mPlaybackState)); 315 } else { 316 mPlaybackStateWrapper.setValue(null); 317 } 318 } 319 } 320 321 /** Convenient extension of {@link PlaybackStateCompat}. */ 322 public static final class PlaybackStateWrapper { 323 324 private final MediaControllerCompat mMediaController; 325 @Nullable 326 private final MediaMetadataCompat mMetadata; 327 private final PlaybackStateCompat mState; 328 PlaybackStateWrapper(@onNull MediaControllerCompat mediaController, @Nullable MediaMetadataCompat metadata, @NonNull PlaybackStateCompat state)329 PlaybackStateWrapper(@NonNull MediaControllerCompat mediaController, 330 @Nullable MediaMetadataCompat metadata, @NonNull PlaybackStateCompat state) { 331 mMediaController = mediaController; 332 mMetadata = metadata; 333 mState = state; 334 } 335 336 /** Returns true if there's enough information in the state to show a UI for it. */ shouldDisplay()337 public boolean shouldDisplay() { 338 // STATE_NONE means no content to play. 339 return mState.getState() != PlaybackStateCompat.STATE_NONE && ((mMetadata != null) || ( 340 getMainAction() != ACTION_DISABLED)); 341 } 342 343 /** Returns the main action. */ 344 @Action getMainAction()345 public int getMainAction() { 346 @Actions long actions = mState.getActions(); 347 @Action int stopAction = ACTION_DISABLED; 348 if ((actions & (PlaybackStateCompat.ACTION_PAUSE 349 | PlaybackStateCompat.ACTION_PLAY_PAUSE)) != 0) { 350 stopAction = ACTION_PAUSE; 351 } else if ((actions & PlaybackStateCompat.ACTION_STOP) != 0) { 352 stopAction = ACTION_STOP; 353 } 354 355 switch (mState.getState()) { 356 case PlaybackStateCompat.STATE_PLAYING: 357 case PlaybackStateCompat.STATE_BUFFERING: 358 case PlaybackStateCompat.STATE_CONNECTING: 359 case PlaybackStateCompat.STATE_FAST_FORWARDING: 360 case PlaybackStateCompat.STATE_REWINDING: 361 case PlaybackStateCompat.STATE_SKIPPING_TO_NEXT: 362 case PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS: 363 case PlaybackStateCompat.STATE_SKIPPING_TO_QUEUE_ITEM: 364 return stopAction; 365 case PlaybackStateCompat.STATE_STOPPED: 366 case PlaybackStateCompat.STATE_PAUSED: 367 case PlaybackStateCompat.STATE_NONE: 368 case PlaybackStateCompat.STATE_ERROR: 369 return (actions & PlaybackStateCompat.ACTION_PLAY) != 0 ? ACTION_PLAY 370 : ACTION_DISABLED; 371 default: 372 Log.w(TAG, String.format("Unknown PlaybackState: %d", mState.getState())); 373 return ACTION_DISABLED; 374 } 375 } 376 377 /** 378 * Returns the currently supported playback actions 379 */ getSupportedActions()380 public long getSupportedActions() { 381 return mState.getActions(); 382 } 383 384 /** 385 * Returns the duration of the media item in milliseconds. The current position in this 386 * duration can be obtained by calling {@link #getProgress()}. 387 */ getMaxProgress()388 public long getMaxProgress() { 389 return mMetadata == null ? 0 : 390 mMetadata.getLong(MediaMetadataCompat.METADATA_KEY_DURATION); 391 } 392 393 /** Returns whether the current media source is playing a media item. */ isPlaying()394 public boolean isPlaying() { 395 return mState.getState() == PlaybackStateCompat.STATE_PLAYING; 396 } 397 398 /** Returns whether the media source supports skipping to the next item. */ isSkipNextEnabled()399 public boolean isSkipNextEnabled() { 400 return (mState.getActions() & PlaybackStateCompat.ACTION_SKIP_TO_NEXT) != 0; 401 } 402 403 /** Returns whether the media source supports skipping to the previous item. */ isSkipPreviousEnabled()404 public boolean isSkipPreviousEnabled() { 405 return (mState.getActions() & PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) != 0; 406 } 407 408 /** 409 * Returns whether the media source supports seeking to a new location in the media stream. 410 */ isSeekToEnabled()411 public boolean isSeekToEnabled() { 412 return (mState.getActions() & PlaybackStateCompat.ACTION_SEEK_TO) != 0; 413 } 414 415 /** Returns whether the media source requires reserved space for the skip to next action. */ isSkipNextReserved()416 public boolean isSkipNextReserved() { 417 return mMediaController.getExtras() != null 418 && (mMediaController.getExtras().getBoolean( 419 MediaConstants.SLOT_RESERVATION_SKIP_TO_NEXT) 420 || mMediaController.getExtras().getBoolean( 421 MediaConstants.PLAYBACK_SLOT_RESERVATION_SKIP_TO_NEXT)); 422 } 423 424 /** 425 * Returns whether the media source requires reserved space for the skip to previous action. 426 */ iSkipPreviousReserved()427 public boolean iSkipPreviousReserved() { 428 return mMediaController.getExtras() != null 429 && (mMediaController.getExtras().getBoolean( 430 MediaConstants.SLOT_RESERVATION_SKIP_TO_PREV) 431 || mMediaController.getExtras().getBoolean( 432 MediaConstants.PLAYBACK_SLOT_RESERVATION_SKIP_TO_PREV)); 433 } 434 435 /** Returns whether the media source is loading (e.g.: buffering, connecting, etc.). */ isLoading()436 public boolean isLoading() { 437 int state = mState.getState(); 438 return state == PlaybackStateCompat.STATE_BUFFERING 439 || state == PlaybackStateCompat.STATE_CONNECTING 440 || state == PlaybackStateCompat.STATE_FAST_FORWARDING 441 || state == PlaybackStateCompat.STATE_REWINDING 442 || state == PlaybackStateCompat.STATE_SKIPPING_TO_NEXT 443 || state == PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS 444 || state == PlaybackStateCompat.STATE_SKIPPING_TO_QUEUE_ITEM; 445 } 446 447 /** See {@link PlaybackStateCompat#getErrorMessage}. */ getErrorMessage()448 public CharSequence getErrorMessage() { 449 return mState.getErrorMessage(); 450 } 451 452 /** See {@link PlaybackStateCompat#getErrorCode()}. */ getErrorCode()453 public int getErrorCode() { 454 return mState.getErrorCode(); 455 } 456 457 /** See {@link PlaybackStateCompat#getActiveQueueItemId}. */ getActiveQueueItemId()458 public long getActiveQueueItemId() { 459 return mState.getActiveQueueItemId(); 460 } 461 462 /** See {@link PlaybackStateCompat#getState}. */ 463 @PlaybackStateCompat.State getState()464 public int getState() { 465 return mState.getState(); 466 } 467 468 /** See {@link PlaybackStateCompat#getExtras}. */ getExtras()469 public Bundle getExtras() { 470 return mState.getExtras(); 471 } 472 473 @VisibleForTesting getStateCompat()474 PlaybackStateCompat getStateCompat() { 475 return mState; 476 } 477 478 /** 479 * Returns a sorted list of custom actions available. Call {@link 480 * RawCustomPlaybackAction#fetchDrawable(Context)} to get the appropriate icon Drawable. 481 */ getCustomActions()482 public List<RawCustomPlaybackAction> getCustomActions() { 483 List<RawCustomPlaybackAction> actions = new ArrayList<>(); 484 RawCustomPlaybackAction ratingAction = getRatingAction(); 485 if (ratingAction != null) actions.add(ratingAction); 486 487 for (PlaybackStateCompat.CustomAction action : mState.getCustomActions()) { 488 String packageName = mMediaController.getPackageName(); 489 actions.add( 490 new RawCustomPlaybackAction(action.getIcon(), packageName, 491 action.getAction(), 492 action.getExtras())); 493 } 494 return actions; 495 } 496 497 @Nullable getRatingAction()498 private RawCustomPlaybackAction getRatingAction() { 499 long stdActions = mState.getActions(); 500 if ((stdActions & PlaybackStateCompat.ACTION_SET_RATING) == 0) return null; 501 502 int ratingType = mMediaController.getRatingType(); 503 if (ratingType != RatingCompat.RATING_HEART) return null; 504 505 boolean hasHeart = false; 506 if (mMetadata != null) { 507 RatingCompat rating = mMetadata.getRating( 508 MediaMetadataCompat.METADATA_KEY_USER_RATING); 509 hasHeart = rating != null && rating.hasHeart(); 510 } 511 512 int iconResource = hasHeart ? R.drawable.ic_star_filled : R.drawable.ic_star_empty; 513 Bundle extras = new Bundle(); 514 extras.putBoolean(EXTRA_SET_HEART, !hasHeart); 515 return new RawCustomPlaybackAction(iconResource, null, ACTION_SET_RATING, extras); 516 } 517 } 518 519 520 /** 521 * Wraps the {@link android.media.session.MediaController.TransportControls TransportControls} 522 * for a {@link MediaControllerCompat} to send commands. 523 */ 524 // TODO(arnaudberry) does this wrapping make sense since we're still null checking the wrapper? 525 // Should we call action methods on the model class instead ? 526 public class PlaybackController { 527 private final MediaControllerCompat mMediaController; 528 PlaybackController(@ullable MediaControllerCompat mediaController)529 private PlaybackController(@Nullable MediaControllerCompat mediaController) { 530 mMediaController = mediaController; 531 } 532 533 /** 534 * Sends a 'play' command to the media source 535 */ play()536 public void play() { 537 if (mMediaController != null) { 538 mMediaController.getTransportControls().play(); 539 } 540 } 541 542 /** 543 * Sends a 'skip previews' command to the media source 544 */ skipToPrevious()545 public void skipToPrevious() { 546 if (mMediaController != null) { 547 mMediaController.getTransportControls().skipToPrevious(); 548 } 549 } 550 551 /** 552 * Sends a 'skip next' command to the media source 553 */ skipToNext()554 public void skipToNext() { 555 if (mMediaController != null) { 556 mMediaController.getTransportControls().skipToNext(); 557 } 558 } 559 560 /** 561 * Sends a 'pause' command to the media source 562 */ pause()563 public void pause() { 564 if (mMediaController != null) { 565 mMediaController.getTransportControls().pause(); 566 } 567 } 568 569 /** 570 * Sends a 'stop' command to the media source 571 */ stop()572 public void stop() { 573 if (mMediaController != null) { 574 mMediaController.getTransportControls().stop(); 575 } 576 } 577 578 /** 579 * Moves to a new location in the media stream 580 * 581 * @param pos Position to move to, in milliseconds. 582 */ seekTo(long pos)583 public void seekTo(long pos) { 584 if (mMediaController != null) { 585 PlaybackStateCompat oldState = mMediaController.getPlaybackState(); 586 PlaybackStateCompat newState = new PlaybackStateCompat.Builder(oldState) 587 .setState(oldState.getState(), pos, oldState.getPlaybackSpeed()) 588 .build(); 589 mMediaControllerCallback.onPlaybackStateChanged(newState); 590 591 mMediaController.getTransportControls().seekTo(pos); 592 } 593 } 594 595 /** 596 * Sends a custom action to the media source 597 * 598 * @param action identifier of the custom action 599 * @param extras additional data to send to the media source. 600 */ doCustomAction(String action, Bundle extras)601 public void doCustomAction(String action, Bundle extras) { 602 if (mMediaController == null) return; 603 MediaControllerCompat.TransportControls cntrl = mMediaController.getTransportControls(); 604 605 if (ACTION_SET_RATING.equals(action)) { 606 boolean setHeart = extras != null && extras.getBoolean(EXTRA_SET_HEART, false); 607 cntrl.setRating(RatingCompat.newHeartRating(setHeart)); 608 } else { 609 cntrl.sendCustomAction(action, extras); 610 } 611 } 612 613 /** 614 * Starts playing a given media item. 615 */ playItem(MediaItemMetadata item)616 public void playItem(MediaItemMetadata item) { 617 if (mMediaController != null) { 618 // Do NOT pass the extras back as that's not the official API and isn't supported 619 // in media2, so apps should not rely on this. 620 mMediaController.getTransportControls().playFromMediaId(item.getId(), null); 621 } 622 } 623 624 /** 625 * Skips to a particular item in the media queue. This id is {@link 626 * MediaItemMetadata#mQueueId} of the items obtained through {@link 627 * PlaybackViewModel#getQueue()}. 628 */ skipToQueueItem(long queueId)629 public void skipToQueueItem(long queueId) { 630 if (mMediaController != null) { 631 mMediaController.getTransportControls().skipToQueueItem(queueId); 632 } 633 } 634 635 /** 636 * Prepares the current media source for playback. 637 */ prepare()638 public void prepare() { 639 if (mMediaController != null) { 640 mMediaController.getTransportControls().prepare(); 641 } 642 } 643 } 644 645 /** 646 * Abstract representation of a custom playback action. A custom playback action represents a 647 * visual element that can be used to trigger playback actions not included in the standard 648 * {@link PlaybackController} class. Custom actions for the current media source are exposed 649 * through {@link PlaybackStateWrapper#getCustomActions} 650 * <p> 651 * Does not contain a {@link Drawable} representation of the icon. Instances of this object 652 * should be converted to a {@link CustomPlaybackAction} via {@link 653 * RawCustomPlaybackAction#fetchDrawable(Context)} for display. 654 */ 655 public static class RawCustomPlaybackAction { 656 // TODO (keyboardr): This class (and associtated translation code) will be merged with 657 // CustomPlaybackAction in a future CL. 658 /** 659 * Icon to display for this custom action 660 */ 661 public final int mIcon; 662 /** 663 * If true, use the resources from the this package to resolve the icon. If null use our own 664 * resources. 665 */ 666 @Nullable 667 public final String mPackageName; 668 /** 669 * Action identifier used to request this action to the media service 670 */ 671 @NonNull 672 public final String mAction; 673 /** 674 * Any additional information to send along with the action identifier 675 */ 676 @Nullable 677 public final Bundle mExtras; 678 679 /** 680 * Creates a custom action 681 */ RawCustomPlaybackAction(int icon, String packageName, @NonNull String action, @Nullable Bundle extras)682 public RawCustomPlaybackAction(int icon, String packageName, 683 @NonNull String action, 684 @Nullable Bundle extras) { 685 mIcon = icon; 686 mPackageName = packageName; 687 mAction = action; 688 mExtras = extras; 689 } 690 691 @Override equals(Object o)692 public boolean equals(Object o) { 693 if (this == o) return true; 694 if (o == null || getClass() != o.getClass()) return false; 695 696 RawCustomPlaybackAction that = (RawCustomPlaybackAction) o; 697 698 return mIcon == that.mIcon 699 && Objects.equals(mPackageName, that.mPackageName) 700 && Objects.equals(mAction, that.mAction) 701 && Objects.equals(mExtras, that.mExtras); 702 } 703 704 @Override hashCode()705 public int hashCode() { 706 return Objects.hash(mIcon, mPackageName, mAction, mExtras); 707 } 708 709 /** 710 * Converts this {@link RawCustomPlaybackAction} into a {@link CustomPlaybackAction} by 711 * fetching the appropriate drawable for the icon. 712 * 713 * @param context Context into which the icon will be drawn 714 * @return the converted CustomPlaybackAction or null if appropriate {@link Resources} 715 * cannot be obtained 716 */ 717 @Nullable fetchDrawable(@onNull Context context)718 public CustomPlaybackAction fetchDrawable(@NonNull Context context) { 719 Drawable icon; 720 if (mPackageName == null) { 721 icon = context.getDrawable(mIcon); 722 } else { 723 Resources resources = getResourcesForPackage(context, mPackageName); 724 if (resources == null) { 725 return null; 726 } else { 727 // the resources may be from another package. we need to update the 728 // configuration 729 // using the context from the activity so we get the drawable from the 730 // correct DPI 731 // bucket. 732 resources.updateConfiguration(context.getResources().getConfiguration(), 733 context.getResources().getDisplayMetrics()); 734 icon = resources.getDrawable(mIcon, null); 735 } 736 } 737 return new CustomPlaybackAction(icon, mAction, mExtras); 738 } 739 getResourcesForPackage(Context context, String packageName)740 private Resources getResourcesForPackage(Context context, String packageName) { 741 try { 742 return context.getPackageManager().getResourcesForApplication(packageName); 743 } catch (PackageManager.NameNotFoundException e) { 744 Log.e(TAG, "Unable to get resources for " + packageName); 745 return null; 746 } 747 } 748 } 749 750 } 751