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