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