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