• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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