Home | History | Annotate | Download | only in common
      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;
     18 
     19 import android.annotation.IntDef;
     20 import android.annotation.NonNull;
     21 import android.annotation.Nullable;
     22 import android.content.Context;
     23 import android.content.pm.PackageManager;
     24 import android.content.res.Resources;
     25 import android.graphics.drawable.Drawable;
     26 import android.media.MediaMetadata;
     27 import android.media.Rating;
     28 import android.media.session.MediaController;
     29 import android.media.session.MediaController.TransportControls;
     30 import android.media.session.MediaSession;
     31 import android.media.session.PlaybackState;
     32 import android.media.session.PlaybackState.Actions;
     33 import android.os.Bundle;
     34 import android.os.Handler;
     35 import android.os.SystemClock;
     36 import android.util.Log;
     37 
     38 import java.lang.annotation.Retention;
     39 import java.lang.annotation.RetentionPolicy;
     40 import java.util.ArrayList;
     41 import java.util.List;
     42 import java.util.function.Consumer;
     43 import java.util.stream.Collectors;
     44 
     45 /**
     46  * Wrapper of {@link MediaSession}. It provides access to media session events and extended
     47  * information on the currently playing item metadata.
     48  */
     49 public class PlaybackModel {
     50     private static final String TAG = "PlaybackModel";
     51 
     52     private static final String ACTION_SET_RATING =
     53             "com.android.car.media.common.ACTION_SET_RATING";
     54     private static final String EXTRA_SET_HEART = "com.android.car.media.common.EXTRA_SET_HEART";
     55 
     56     private final Handler mHandler = new Handler();
     57     @Nullable
     58     private final Context mContext;
     59     private final List<PlaybackObserver> mObservers = new ArrayList<>();
     60     private MediaController mMediaController;
     61     private MediaSource mMediaSource;
     62     private boolean mIsStarted;
     63 
     64     /**
     65      * An observer of this model
     66      */
     67     public abstract static class PlaybackObserver {
     68         /**
     69          * Called whenever the playback state of the current media item changes.
     70          */
     71         protected void onPlaybackStateChanged() {};
     72 
     73         /**
     74          * Called when the top source media app changes.
     75          */
     76         protected void onSourceChanged() {};
     77 
     78         /**
     79          * Called when the media item being played changes.
     80          */
     81         protected void onMetadataChanged() {};
     82     }
     83 
     84     private MediaController.Callback mCallback = new MediaController.Callback() {
     85         @Override
     86         public void onPlaybackStateChanged(PlaybackState state) {
     87             if (Log.isLoggable(TAG, Log.DEBUG)) {
     88                 Log.d(TAG, "onPlaybackStateChanged: " + state);
     89             }
     90             PlaybackModel.this.notify(PlaybackObserver::onPlaybackStateChanged);
     91         }
     92 
     93         @Override
     94         public void onMetadataChanged(MediaMetadata metadata) {
     95             if (Log.isLoggable(TAG, Log.DEBUG)) {
     96                 Log.d(TAG, "onMetadataChanged: " + metadata);
     97             }
     98             PlaybackModel.this.notify(PlaybackObserver::onMetadataChanged);
     99         }
    100     };
    101 
    102     /**
    103      * Creates a {@link PlaybackModel}
    104      */
    105     public PlaybackModel(@NonNull Context context) {
    106        this(context, null);
    107     }
    108 
    109     /**
    110      * Creates a {@link PlaybackModel} wrapping to the given media controller
    111      */
    112     public PlaybackModel(@NonNull Context context, @Nullable MediaController controller) {
    113         mContext = context;
    114         changeMediaController(controller);
    115     }
    116 
    117     /**
    118      * Sets the {@link MediaController} wrapped by this model.
    119      */
    120     public void setMediaController(@Nullable MediaController mediaController) {
    121         changeMediaController(mediaController);
    122     }
    123 
    124     private void changeMediaController(@Nullable MediaController mediaController) {
    125         if (Log.isLoggable(TAG, Log.DEBUG)) {
    126             Log.d(TAG, "New media controller: " + (mediaController != null
    127                     ? mediaController.getPackageName() : null));
    128         }
    129         if ((mediaController == null && mMediaController == null)
    130                 || (mediaController != null && mMediaController != null
    131                 && mediaController.getPackageName().equals(mMediaController.getPackageName()))) {
    132             // If no change, do nothing.
    133             return;
    134         }
    135         if (mMediaController != null) {
    136             mMediaController.unregisterCallback(mCallback);
    137         }
    138         mMediaController = mediaController;
    139         mMediaSource = mMediaController != null
    140             ? new MediaSource(mContext, mMediaController.getPackageName()) : null;
    141         if (mMediaController != null && mIsStarted) {
    142             mMediaController.registerCallback(mCallback);
    143         }
    144         if (mIsStarted) {
    145             notify(PlaybackObserver::onSourceChanged);
    146         }
    147     }
    148 
    149     /**
    150      * Starts following changes on the playback state of the given source. If any changes happen,
    151      * all observers registered through {@link #registerObserver(PlaybackObserver)} will be
    152      * notified.
    153      */
    154     private void start() {
    155         if (mMediaController != null) {
    156             mMediaController.registerCallback(mCallback);
    157         }
    158         mIsStarted = true;
    159     }
    160 
    161     /**
    162      * Stops following changes on the list of active media sources.
    163      */
    164     private void stop() {
    165         if (mMediaController != null) {
    166             mMediaController.unregisterCallback(mCallback);
    167         }
    168         mIsStarted = false;
    169     }
    170 
    171     private void notify(Consumer<PlaybackObserver> notification) {
    172         mHandler.post(() -> {
    173             List<PlaybackObserver> observers = new ArrayList<>(mObservers);
    174             for (PlaybackObserver observer : observers) {
    175                 notification.accept(observer);
    176             }
    177         });
    178     }
    179 
    180     /**
    181      * @return a {@link MediaSource} providing access to metadata of the currently playing media
    182      * source, or NULL if the media source has no active session.
    183      */
    184     @Nullable
    185     public MediaSource getMediaSource() {
    186         return mMediaSource;
    187     }
    188 
    189     /**
    190      * @return a {@link MediaController} that can be used to control this media source, or NULL
    191      * if the media source has no active session.
    192      */
    193     @Nullable
    194     public MediaController getMediaController() {
    195         return mMediaController;
    196     }
    197 
    198     /**
    199      * @return {@link Action} selected as the main action for the current media item, based on the
    200      * current playback state and the available actions reported by the media source.
    201      * Changes on this value will be notified through
    202      * {@link PlaybackObserver#onPlaybackStateChanged()}
    203      */
    204     @Action
    205     public int getMainAction() {
    206         return getMainAction(mMediaController != null ? mMediaController.getPlaybackState() : null);
    207     }
    208 
    209     /**
    210      * @return {@link MediaItemMetadata} of the currently selected media item in the media source.
    211      * Changes on this value will be notified through {@link PlaybackObserver#onMetadataChanged()}
    212      */
    213     @Nullable
    214     public MediaItemMetadata getMetadata() {
    215         if (mMediaController == null) {
    216             return null;
    217         }
    218         MediaMetadata metadata = mMediaController.getMetadata();
    219         if (metadata == null) {
    220             return null;
    221         }
    222         return new MediaItemMetadata(metadata);
    223     }
    224 
    225     /**
    226      * @return duration of the media item, in milliseconds. The current position in this duration
    227      * can be obtained by calling {@link #getProgress()}.
    228      * Changes on this value will be notified through {@link PlaybackObserver#onMetadataChanged()}
    229      */
    230     public long getMaxProgress() {
    231         if (mMediaController == null || mMediaController.getMetadata() == null) {
    232             return 0;
    233         } else {
    234             return mMediaController.getMetadata()
    235                     .getLong(MediaMetadata.METADATA_KEY_DURATION);
    236         }
    237     }
    238 
    239     /**
    240      * Sends a 'play' command to the media source
    241      */
    242     public void onPlay() {
    243         if (mMediaController != null) {
    244             mMediaController.getTransportControls().play();
    245         }
    246     }
    247 
    248     /**
    249      * Sends a 'skip previews' command to the media source
    250      */
    251     public void onSkipPreviews() {
    252         if (mMediaController != null) {
    253             mMediaController.getTransportControls().skipToPrevious();
    254         }
    255     }
    256 
    257     /**
    258      * Sends a 'skip next' command to the media source
    259      */
    260     public void onSkipNext() {
    261         if (mMediaController != null) {
    262             mMediaController.getTransportControls().skipToNext();
    263         }
    264     }
    265 
    266     /**
    267      * Sends a 'pause' command to the media source
    268      */
    269     public void onPause() {
    270         if (mMediaController != null) {
    271             mMediaController.getTransportControls().pause();
    272         }
    273     }
    274 
    275     /**
    276      * Sends a 'stop' command to the media source
    277      */
    278     public void onStop() {
    279         if (mMediaController != null) {
    280             mMediaController.getTransportControls().stop();
    281         }
    282     }
    283 
    284     /**
    285      * Sends a custom action to the media source
    286      * @param action identifier of the custom action
    287      * @param extras additional data to send to the media source.
    288      */
    289     public void onCustomAction(String action, Bundle extras) {
    290         if (mMediaController == null) return;
    291         TransportControls cntrl = mMediaController.getTransportControls();
    292 
    293         if (ACTION_SET_RATING.equals(action)) {
    294             boolean setHeart = extras != null && extras.getBoolean(EXTRA_SET_HEART, false);
    295             cntrl.setRating(Rating.newHeartRating(setHeart));
    296         } else {
    297             cntrl.sendCustomAction(action, extras);
    298         }
    299 
    300         mMediaController.getTransportControls().sendCustomAction(action, extras);
    301     }
    302 
    303     /**
    304      * Starts playing a given media item. This id corresponds to {@link MediaItemMetadata#getId()}.
    305      */
    306     public void onPlayItem(String mediaItemId) {
    307         if (mMediaController != null) {
    308             mMediaController.getTransportControls().playFromMediaId(mediaItemId, null);
    309         }
    310     }
    311 
    312     /**
    313      * Skips to a particular item in the media queue. This id is {@link MediaItemMetadata#mQueueId}
    314      * of the items obtained through {@link #getQueue()}.
    315      */
    316     public void onSkipToQueueItem(long queueId) {
    317         if (mMediaController != null) {
    318             mMediaController.getTransportControls().skipToQueueItem(queueId);
    319         }
    320     }
    321 
    322     /**
    323      * Prepares the current media source for playback.
    324      */
    325     public void onPrepare() {
    326         if (mMediaController != null) {
    327             mMediaController.getTransportControls().prepare();
    328         }
    329     }
    330 
    331     /**
    332      * Possible main actions.
    333      */
    334     @IntDef({ACTION_PLAY, ACTION_STOP, ACTION_PAUSE, ACTION_DISABLED})
    335     @Retention(RetentionPolicy.SOURCE)
    336     public @interface Action {}
    337 
    338     /** Main action is disabled. The source can't play media at this time */
    339     public static final int ACTION_DISABLED = 0;
    340     /** Start playing */
    341     public static final int ACTION_PLAY = 1;
    342     /** Stop playing */
    343     public static final int ACTION_STOP = 2;
    344     /** Pause playing */
    345     public static final int ACTION_PAUSE = 3;
    346 
    347     @Action
    348     private static int getMainAction(PlaybackState state) {
    349         if (state == null) {
    350             return ACTION_DISABLED;
    351         }
    352 
    353         @Actions long actions = state.getActions();
    354         int stopAction = ACTION_DISABLED;
    355         if ((actions & (PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_PLAY_PAUSE)) != 0) {
    356             stopAction = ACTION_PAUSE;
    357         } else if ((actions & PlaybackState.ACTION_STOP) != 0) {
    358             stopAction = ACTION_STOP;
    359         }
    360 
    361         switch (state.getState()) {
    362             case PlaybackState.STATE_PLAYING:
    363             case PlaybackState.STATE_BUFFERING:
    364             case PlaybackState.STATE_CONNECTING:
    365             case PlaybackState.STATE_FAST_FORWARDING:
    366             case PlaybackState.STATE_REWINDING:
    367             case PlaybackState.STATE_SKIPPING_TO_NEXT:
    368             case PlaybackState.STATE_SKIPPING_TO_PREVIOUS:
    369             case PlaybackState.STATE_SKIPPING_TO_QUEUE_ITEM:
    370                 return stopAction;
    371             case PlaybackState.STATE_STOPPED:
    372             case PlaybackState.STATE_PAUSED:
    373             case PlaybackState.STATE_NONE:
    374                 return ACTION_PLAY;
    375             case PlaybackState.STATE_ERROR:
    376                 return ACTION_DISABLED;
    377             default:
    378                 Log.w(TAG, String.format("Unknown PlaybackState: %d", state.getState()));
    379                 return ACTION_DISABLED;
    380         }
    381     }
    382 
    383     /**
    384      * @return the current playback progress, in milliseconds. This is a value between 0 and
    385      * {@link #getMaxProgress()} or PROGRESS_UNKNOWN of the current position is unknown.
    386      */
    387     public long getProgress() {
    388         if (mMediaController == null) {
    389             return 0;
    390         }
    391         PlaybackState state = mMediaController.getPlaybackState();
    392         if (state == null) {
    393             return 0;
    394         }
    395         if (state.getPosition() == PlaybackState.PLAYBACK_POSITION_UNKNOWN) {
    396             return PlaybackState.PLAYBACK_POSITION_UNKNOWN;
    397         }
    398         long timeDiff = SystemClock.elapsedRealtime() - state.getLastPositionUpdateTime();
    399         float speed = state.getPlaybackSpeed();
    400         if (state.getState() == PlaybackState.STATE_PAUSED
    401                 || state.getState() == PlaybackState.STATE_STOPPED) {
    402             // This guards against apps who don't keep their playbackSpeed to spec (b/62375164)
    403             speed = 0f;
    404         }
    405         long posDiff = (long) (timeDiff * speed);
    406         return Math.min(posDiff + state.getPosition(), getMaxProgress());
    407     }
    408 
    409     /**
    410      * @return true if the current media source is playing a media item. Changes on this value
    411      * would be notified through {@link PlaybackObserver#onPlaybackStateChanged()}
    412      */
    413     public boolean isPlaying() {
    414         return mMediaController != null
    415                 && mMediaController.getPlaybackState() != null
    416                 && mMediaController.getPlaybackState().getState() == PlaybackState.STATE_PLAYING;
    417     }
    418 
    419     /**
    420      * Registers an observer to be notified of media events. If the model is not started yet it
    421      * will start right away. If the model was already started, the observer will receive an
    422      * immediate {@link PlaybackObserver#onSourceChanged()} event.
    423      */
    424     public void registerObserver(PlaybackObserver observer) {
    425         mObservers.add(observer);
    426         if (!mIsStarted) {
    427             start();
    428         } else {
    429             observer.onSourceChanged();
    430         }
    431     }
    432 
    433     /**
    434      * Unregisters an observer previously registered using
    435      * {@link #registerObserver(PlaybackObserver)}. There are no other observers the model will
    436      * stop tracking changes right away.
    437      */
    438     public void unregisterObserver(PlaybackObserver observer) {
    439         mObservers.remove(observer);
    440         if (mObservers.isEmpty() && mIsStarted) {
    441             stop();
    442         }
    443     }
    444 
    445     /**
    446      * @return true if the media source supports skipping to next item. Changes on this value
    447      * will be notified through {@link PlaybackObserver#onPlaybackStateChanged()}
    448      */
    449     public boolean isSkipNextEnabled() {
    450         return mMediaController != null
    451                 && mMediaController.getPlaybackState() != null
    452                 && (mMediaController.getPlaybackState().getActions()
    453                     & PlaybackState.ACTION_SKIP_TO_NEXT) != 0;
    454     }
    455 
    456     /**
    457      * @return true if the media source supports skipping to previous item. Changes on this value
    458      * will be notified through {@link PlaybackObserver#onPlaybackStateChanged()}
    459      */
    460     public boolean isSkipPreviewsEnabled() {
    461         return mMediaController != null
    462                 && mMediaController.getPlaybackState() != null
    463                 && (mMediaController.getPlaybackState().getActions()
    464                     & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0;
    465     }
    466 
    467     /**
    468      * @return true if the media source is buffering. Changes on this value would be notified
    469      * through {@link PlaybackObserver#onPlaybackStateChanged()}
    470      */
    471     public boolean isBuffering() {
    472         return mMediaController != null
    473                 && mMediaController.getPlaybackState() != null
    474                 && mMediaController.getPlaybackState().getState() == PlaybackState.STATE_BUFFERING;
    475     }
    476 
    477     /**
    478      * @return a human readable description of the error that cause the media source to be in a
    479      * non-playable state, or null if there is no error. Changes on this value will be notified
    480      * through {@link PlaybackObserver#onPlaybackStateChanged()}
    481      */
    482     @Nullable
    483     public CharSequence getErrorMessage() {
    484         return mMediaController != null && mMediaController.getPlaybackState() != null
    485                 ? mMediaController.getPlaybackState().getErrorMessage()
    486                 : null;
    487     }
    488 
    489     /**
    490      * @return a sorted list of {@link MediaItemMetadata} corresponding to the queue of media items
    491      * as reported by the media source. Changes on this value will be notified through
    492      * {@link PlaybackObserver#onPlaybackStateChanged()}.
    493      */
    494     @NonNull
    495     public List<MediaItemMetadata> getQueue() {
    496         if (mMediaController == null) {
    497             return new ArrayList<>();
    498         }
    499         List<MediaSession.QueueItem> items = mMediaController.getQueue();
    500         if (items != null) {
    501             return items.stream()
    502                     .filter(item -> item.getDescription() != null
    503                         && item.getDescription().getTitle() != null)
    504                     .map(MediaItemMetadata::new)
    505                     .collect(Collectors.toList());
    506         } else {
    507             return new ArrayList<>();
    508         }
    509     }
    510 
    511     /**
    512      * @return the title of the queue or NULL if not available.
    513      */
    514     @Nullable
    515     public CharSequence getQueueTitle() {
    516         if (mMediaController == null) {
    517             return null;
    518         }
    519         return mMediaController.getQueueTitle();
    520     }
    521 
    522     /**
    523      * @return queue id of the currently playing queue item, or
    524      * {@link MediaSession.QueueItem#UNKNOWN_ID} if none of the items is currently playing.
    525      */
    526     public long getActiveQueueItemId() {
    527         PlaybackState playbackState = mMediaController.getPlaybackState();
    528         if (playbackState == null) return MediaSession.QueueItem.UNKNOWN_ID;
    529         return playbackState.getActiveQueueItemId();
    530     }
    531 
    532     /**
    533      * @return true if the media queue is not empty. Detailed information can be obtained by
    534      * calling to {@link #getQueue()}. Changes on this value will be notified through
    535      * {@link PlaybackObserver#onPlaybackStateChanged()}.
    536      */
    537     public boolean hasQueue() {
    538         if (mMediaController == null) {
    539             return false;
    540         }
    541         List<MediaSession.QueueItem> items = mMediaController.getQueue();
    542         return items != null && !items.isEmpty();
    543     }
    544 
    545     private @Nullable CustomPlaybackAction getRatingAction() {
    546         PlaybackState playbackState = mMediaController.getPlaybackState();
    547         if (playbackState == null) return null;
    548 
    549         long stdActions = playbackState.getActions();
    550         if ((stdActions & PlaybackState.ACTION_SET_RATING) == 0) return null;
    551 
    552         int ratingType = mMediaController.getRatingType();
    553         if (ratingType != Rating.RATING_HEART) return null;
    554 
    555         MediaMetadata metadata = mMediaController.getMetadata();
    556         boolean hasHeart = false;
    557         if (metadata != null) {
    558             Rating rating = metadata.getRating(MediaMetadata.METADATA_KEY_USER_RATING);
    559             hasHeart = rating != null && rating.hasHeart();
    560         }
    561 
    562         int iconResource = hasHeart ? R.drawable.ic_star_filled : R.drawable.ic_star_empty;
    563         Drawable icon = mContext.getResources().getDrawable(iconResource, null);
    564         Bundle extras = new Bundle();
    565         extras.putBoolean(EXTRA_SET_HEART, !hasHeart);
    566         return new CustomPlaybackAction(icon, ACTION_SET_RATING, extras);
    567     }
    568 
    569     /**
    570      * @return a sorted list of custom actions, as reported by the media source. Changes on this
    571      * value will be notified through
    572      * {@link PlaybackObserver#onPlaybackStateChanged()}.
    573      */
    574     public List<CustomPlaybackAction> getCustomActions() {
    575         List<CustomPlaybackAction> actions = new ArrayList<>();
    576         if (mMediaController == null) return actions;
    577         PlaybackState playbackState = mMediaController.getPlaybackState();
    578         if (playbackState == null) return actions;
    579 
    580         CustomPlaybackAction ratingAction = getRatingAction();
    581         if (ratingAction != null) actions.add(ratingAction);
    582 
    583         for (PlaybackState.CustomAction action : playbackState.getCustomActions()) {
    584             Resources resources = getResourcesForPackage(mMediaController.getPackageName());
    585             if (resources == null) {
    586                 actions.add(null);
    587             } else {
    588                 // the resources may be from another package. we need to update the configuration
    589                 // using the context from the activity so we get the drawable from the correct DPI
    590                 // bucket.
    591                 resources.updateConfiguration(mContext.getResources().getConfiguration(),
    592                         mContext.getResources().getDisplayMetrics());
    593                 Drawable icon = resources.getDrawable(action.getIcon(), null);
    594                 actions.add(new CustomPlaybackAction(icon, action.getAction(), action.getExtras()));
    595             }
    596         }
    597         return actions;
    598     }
    599 
    600     private Resources getResourcesForPackage(String packageName) {
    601         try {
    602             return mContext.getPackageManager().getResourcesForApplication(packageName);
    603         } catch (PackageManager.NameNotFoundException e) {
    604             Log.e(TAG, "Unable to get resources for " + packageName);
    605             return null;
    606         }
    607     }
    608 }
    609