Home | History | Annotate | Download | only in session
      1 /*
      2  * Copyright (C) 2014 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 android.media.session;
     18 
     19 import android.annotation.IntDef;
     20 import android.annotation.NonNull;
     21 import android.annotation.Nullable;
     22 import android.app.Activity;
     23 import android.app.PendingIntent;
     24 import android.content.Context;
     25 import android.content.Intent;
     26 import android.content.pm.ParceledListSlice;
     27 import android.media.AudioAttributes;
     28 import android.media.MediaDescription;
     29 import android.media.MediaMetadata;
     30 import android.media.Rating;
     31 import android.media.VolumeProvider;
     32 import android.net.Uri;
     33 import android.os.Bundle;
     34 import android.os.Handler;
     35 import android.os.Looper;
     36 import android.os.Message;
     37 import android.os.Parcel;
     38 import android.os.Parcelable;
     39 import android.os.RemoteException;
     40 import android.os.ResultReceiver;
     41 import android.os.UserHandle;
     42 import android.service.media.MediaBrowserService;
     43 import android.text.TextUtils;
     44 import android.util.Log;
     45 import android.view.KeyEvent;
     46 
     47 import java.lang.annotation.Retention;
     48 import java.lang.annotation.RetentionPolicy;
     49 import java.lang.ref.WeakReference;
     50 import java.util.List;
     51 
     52 /**
     53  * Allows interaction with media controllers, volume keys, media buttons, and
     54  * transport controls.
     55  * <p>
     56  * A MediaSession should be created when an app wants to publish media playback
     57  * information or handle media keys. In general an app only needs one session
     58  * for all playback, though multiple sessions can be created to provide finer
     59  * grain controls of media.
     60  * <p>
     61  * Once a session is created the owner of the session may pass its
     62  * {@link #getSessionToken() session token} to other processes to allow them to
     63  * create a {@link MediaController} to interact with the session.
     64  * <p>
     65  * To receive commands, media keys, and other events a {@link Callback} must be
     66  * set with {@link #setCallback(Callback)} and {@link #setActive(boolean)
     67  * setActive(true)} must be called.
     68  * <p>
     69  * When an app is finished performing playback it must call {@link #release()}
     70  * to clean up the session and notify any controllers.
     71  * <p>
     72  * MediaSession objects are thread safe.
     73  */
     74 public final class MediaSession {
     75     private static final String TAG = "MediaSession";
     76 
     77     /**
     78      * Set this flag on the session to indicate that it can handle media button
     79      * events.
     80      */
     81     public static final int FLAG_HANDLES_MEDIA_BUTTONS = 1 << 0;
     82 
     83     /**
     84      * Set this flag on the session to indicate that it handles transport
     85      * control commands through its {@link Callback}.
     86      */
     87     public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 1 << 1;
     88 
     89     /**
     90      * System only flag for a session that needs to have priority over all other
     91      * sessions. This flag ensures this session will receive media button events
     92      * regardless of the current ordering in the system.
     93      *
     94      * @hide
     95      */
     96     public static final int FLAG_EXCLUSIVE_GLOBAL_PRIORITY = 1 << 16;
     97 
     98     /** @hide */
     99     @Retention(RetentionPolicy.SOURCE)
    100     @IntDef(flag = true, value = {
    101             FLAG_HANDLES_MEDIA_BUTTONS,
    102             FLAG_HANDLES_TRANSPORT_CONTROLS,
    103             FLAG_EXCLUSIVE_GLOBAL_PRIORITY })
    104     public @interface SessionFlags { }
    105 
    106     private final Object mLock = new Object();
    107     private final int mMaxBitmapSize;
    108 
    109     private final MediaSession.Token mSessionToken;
    110     private final MediaController mController;
    111     private final ISession mBinder;
    112     private final CallbackStub mCbStub;
    113 
    114     private CallbackMessageHandler mCallback;
    115     private VolumeProvider mVolumeProvider;
    116     private PlaybackState mPlaybackState;
    117 
    118     private boolean mActive = false;
    119 
    120     /**
    121      * Creates a new session. The session will automatically be registered with
    122      * the system but will not be published until {@link #setActive(boolean)
    123      * setActive(true)} is called. You must call {@link #release()} when
    124      * finished with the session.
    125      *
    126      * @param context The context to use to create the session.
    127      * @param tag A short name for debugging purposes.
    128      */
    129     public MediaSession(@NonNull Context context, @NonNull String tag) {
    130         this(context, tag, UserHandle.myUserId());
    131     }
    132 
    133     /**
    134      * Creates a new session as the specified user. To create a session as a
    135      * user other than your own you must hold the
    136      * {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL}
    137      * permission.
    138      *
    139      * @param context The context to use to create the session.
    140      * @param tag A short name for debugging purposes.
    141      * @param userId The user id to create the session as.
    142      * @hide
    143      */
    144     public MediaSession(@NonNull Context context, @NonNull String tag, int userId) {
    145         if (context == null) {
    146             throw new IllegalArgumentException("context cannot be null.");
    147         }
    148         if (TextUtils.isEmpty(tag)) {
    149             throw new IllegalArgumentException("tag cannot be null or empty");
    150         }
    151         mMaxBitmapSize = context.getResources().getDimensionPixelSize(
    152                 com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize);
    153         mCbStub = new CallbackStub(this);
    154         MediaSessionManager manager = (MediaSessionManager) context
    155                 .getSystemService(Context.MEDIA_SESSION_SERVICE);
    156         try {
    157             mBinder = manager.createSession(mCbStub, tag, userId);
    158             mSessionToken = new Token(mBinder.getController());
    159             mController = new MediaController(context, mSessionToken);
    160         } catch (RemoteException e) {
    161             throw new RuntimeException("Remote error creating session.", e);
    162         }
    163     }
    164 
    165     /**
    166      * Set the callback to receive updates for the MediaSession. This includes
    167      * media button events and transport controls. The caller's thread will be
    168      * used to post updates.
    169      * <p>
    170      * Set the callback to null to stop receiving updates.
    171      *
    172      * @param callback The callback object
    173      */
    174     public void setCallback(@Nullable Callback callback) {
    175         setCallback(callback, null);
    176     }
    177 
    178     /**
    179      * Set the callback to receive updates for the MediaSession. This includes
    180      * media button events and transport controls.
    181      * <p>
    182      * Set the callback to null to stop receiving updates.
    183      *
    184      * @param callback The callback to receive updates on.
    185      * @param handler The handler that events should be posted on.
    186      */
    187     public void setCallback(@Nullable Callback callback, @Nullable Handler handler) {
    188         synchronized (mLock) {
    189             if (callback == null) {
    190                 if (mCallback != null) {
    191                     mCallback.mCallback.mSession = null;
    192                 }
    193                 mCallback = null;
    194                 return;
    195             }
    196             if (mCallback != null) {
    197                 // We're updating the callback, clear the session from the old
    198                 // one.
    199                 mCallback.mCallback.mSession = null;
    200             }
    201             if (handler == null) {
    202                 handler = new Handler();
    203             }
    204             callback.mSession = this;
    205             CallbackMessageHandler msgHandler = new CallbackMessageHandler(handler.getLooper(),
    206                     callback);
    207             mCallback = msgHandler;
    208         }
    209     }
    210 
    211     /**
    212      * Set an intent for launching UI for this Session. This can be used as a
    213      * quick link to an ongoing media screen. The intent should be for an
    214      * activity that may be started using {@link Activity#startActivity(Intent)}.
    215      *
    216      * @param pi The intent to launch to show UI for this Session.
    217      */
    218     public void setSessionActivity(@Nullable PendingIntent pi) {
    219         try {
    220             mBinder.setLaunchPendingIntent(pi);
    221         } catch (RemoteException e) {
    222             Log.wtf(TAG, "Failure in setLaunchPendingIntent.", e);
    223         }
    224     }
    225 
    226     /**
    227      * Set a pending intent for your media button receiver to allow restarting
    228      * playback after the session has been stopped. If your app is started in
    229      * this way an {@link Intent#ACTION_MEDIA_BUTTON} intent will be sent via
    230      * the pending intent.
    231      *
    232      * @param mbr The {@link PendingIntent} to send the media button event to.
    233      */
    234     public void setMediaButtonReceiver(@Nullable PendingIntent mbr) {
    235         try {
    236             mBinder.setMediaButtonReceiver(mbr);
    237         } catch (RemoteException e) {
    238             Log.wtf(TAG, "Failure in setMediaButtonReceiver.", e);
    239         }
    240     }
    241 
    242     /**
    243      * Set any flags for the session.
    244      *
    245      * @param flags The flags to set for this session.
    246      */
    247     public void setFlags(@SessionFlags int flags) {
    248         try {
    249             mBinder.setFlags(flags);
    250         } catch (RemoteException e) {
    251             Log.wtf(TAG, "Failure in setFlags.", e);
    252         }
    253     }
    254 
    255     /**
    256      * Set the attributes for this session's audio. This will affect the
    257      * system's volume handling for this session. If
    258      * {@link #setPlaybackToRemote} was previously called it will stop receiving
    259      * volume commands and the system will begin sending volume changes to the
    260      * appropriate stream.
    261      * <p>
    262      * By default sessions use attributes for media.
    263      *
    264      * @param attributes The {@link AudioAttributes} for this session's audio.
    265      */
    266     public void setPlaybackToLocal(AudioAttributes attributes) {
    267         if (attributes == null) {
    268             throw new IllegalArgumentException("Attributes cannot be null for local playback.");
    269         }
    270         try {
    271             mBinder.setPlaybackToLocal(attributes);
    272         } catch (RemoteException e) {
    273             Log.wtf(TAG, "Failure in setPlaybackToLocal.", e);
    274         }
    275     }
    276 
    277     /**
    278      * Configure this session to use remote volume handling. This must be called
    279      * to receive volume button events, otherwise the system will adjust the
    280      * appropriate stream volume for this session. If
    281      * {@link #setPlaybackToLocal} was previously called the system will stop
    282      * handling volume changes for this session and pass them to the volume
    283      * provider instead.
    284      *
    285      * @param volumeProvider The provider that will handle volume changes. May
    286      *            not be null.
    287      */
    288     public void setPlaybackToRemote(@NonNull VolumeProvider volumeProvider) {
    289         if (volumeProvider == null) {
    290             throw new IllegalArgumentException("volumeProvider may not be null!");
    291         }
    292         synchronized (mLock) {
    293             mVolumeProvider = volumeProvider;
    294         }
    295         volumeProvider.setCallback(new VolumeProvider.Callback() {
    296             @Override
    297             public void onVolumeChanged(VolumeProvider volumeProvider) {
    298                 notifyRemoteVolumeChanged(volumeProvider);
    299             }
    300         });
    301 
    302         try {
    303             mBinder.setPlaybackToRemote(volumeProvider.getVolumeControl(),
    304                     volumeProvider.getMaxVolume());
    305             mBinder.setCurrentVolume(volumeProvider.getCurrentVolume());
    306         } catch (RemoteException e) {
    307             Log.wtf(TAG, "Failure in setPlaybackToRemote.", e);
    308         }
    309     }
    310 
    311     /**
    312      * Set if this session is currently active and ready to receive commands. If
    313      * set to false your session's controller may not be discoverable. You must
    314      * set the session to active before it can start receiving media button
    315      * events or transport commands.
    316      *
    317      * @param active Whether this session is active or not.
    318      */
    319     public void setActive(boolean active) {
    320         if (mActive == active) {
    321             return;
    322         }
    323         try {
    324             mBinder.setActive(active);
    325             mActive = active;
    326         } catch (RemoteException e) {
    327             Log.wtf(TAG, "Failure in setActive.", e);
    328         }
    329     }
    330 
    331     /**
    332      * Get the current active state of this session.
    333      *
    334      * @return True if the session is active, false otherwise.
    335      */
    336     public boolean isActive() {
    337         return mActive;
    338     }
    339 
    340     /**
    341      * Send a proprietary event to all MediaControllers listening to this
    342      * Session. It's up to the Controller/Session owner to determine the meaning
    343      * of any events.
    344      *
    345      * @param event The name of the event to send
    346      * @param extras Any extras included with the event
    347      */
    348     public void sendSessionEvent(@NonNull String event, @Nullable Bundle extras) {
    349         if (TextUtils.isEmpty(event)) {
    350             throw new IllegalArgumentException("event cannot be null or empty");
    351         }
    352         try {
    353             mBinder.sendEvent(event, extras);
    354         } catch (RemoteException e) {
    355             Log.wtf(TAG, "Error sending event", e);
    356         }
    357     }
    358 
    359     /**
    360      * This must be called when an app has finished performing playback. If
    361      * playback is expected to start again shortly the session can be left open,
    362      * but it must be released if your activity or service is being destroyed.
    363      */
    364     public void release() {
    365         try {
    366             mBinder.destroy();
    367         } catch (RemoteException e) {
    368             Log.wtf(TAG, "Error releasing session: ", e);
    369         }
    370     }
    371 
    372     /**
    373      * Retrieve a token object that can be used by apps to create a
    374      * {@link MediaController} for interacting with this session. The owner of
    375      * the session is responsible for deciding how to distribute these tokens.
    376      *
    377      * @return A token that can be used to create a MediaController for this
    378      *         session
    379      */
    380     public @NonNull Token getSessionToken() {
    381         return mSessionToken;
    382     }
    383 
    384     /**
    385      * Get a controller for this session. This is a convenience method to avoid
    386      * having to cache your own controller in process.
    387      *
    388      * @return A controller for this session.
    389      */
    390     public @NonNull MediaController getController() {
    391         return mController;
    392     }
    393 
    394     /**
    395      * Update the current playback state.
    396      *
    397      * @param state The current state of playback
    398      */
    399     public void setPlaybackState(@Nullable PlaybackState state) {
    400         mPlaybackState = state;
    401         try {
    402             mBinder.setPlaybackState(state);
    403         } catch (RemoteException e) {
    404             Log.wtf(TAG, "Dead object in setPlaybackState.", e);
    405         }
    406     }
    407 
    408     /**
    409      * Update the current metadata. New metadata can be created using
    410      * {@link android.media.MediaMetadata.Builder}.
    411      *
    412      * @param metadata The new metadata
    413      */
    414     public void setMetadata(@Nullable MediaMetadata metadata) {
    415         if (metadata != null ) {
    416             metadata = (new MediaMetadata.Builder(metadata, mMaxBitmapSize)).build();
    417         }
    418         try {
    419             mBinder.setMetadata(metadata);
    420         } catch (RemoteException e) {
    421             Log.wtf(TAG, "Dead object in setPlaybackState.", e);
    422         }
    423     }
    424 
    425     /**
    426      * Update the list of items in the play queue. It is an ordered list and
    427      * should contain the current item, and previous or upcoming items if they
    428      * exist. Specify null if there is no current play queue.
    429      * <p>
    430      * The queue should be of reasonable size. If the play queue is unbounded
    431      * within your app, it is better to send a reasonable amount in a sliding
    432      * window instead.
    433      *
    434      * @param queue A list of items in the play queue.
    435      */
    436     public void setQueue(@Nullable List<QueueItem> queue) {
    437         try {
    438             mBinder.setQueue(queue == null ? null : new ParceledListSlice<QueueItem>(queue));
    439         } catch (RemoteException e) {
    440             Log.wtf("Dead object in setQueue.", e);
    441         }
    442     }
    443 
    444     /**
    445      * Set the title of the play queue. The UI should display this title along
    446      * with the play queue itself.
    447      * e.g. "Play Queue", "Now Playing", or an album name.
    448      *
    449      * @param title The title of the play queue.
    450      */
    451     public void setQueueTitle(@Nullable CharSequence title) {
    452         try {
    453             mBinder.setQueueTitle(title);
    454         } catch (RemoteException e) {
    455             Log.wtf("Dead object in setQueueTitle.", e);
    456         }
    457     }
    458 
    459     /**
    460      * Set the style of rating used by this session. Apps trying to set the
    461      * rating should use this style. Must be one of the following:
    462      * <ul>
    463      * <li>{@link Rating#RATING_NONE}</li>
    464      * <li>{@link Rating#RATING_3_STARS}</li>
    465      * <li>{@link Rating#RATING_4_STARS}</li>
    466      * <li>{@link Rating#RATING_5_STARS}</li>
    467      * <li>{@link Rating#RATING_HEART}</li>
    468      * <li>{@link Rating#RATING_PERCENTAGE}</li>
    469      * <li>{@link Rating#RATING_THUMB_UP_DOWN}</li>
    470      * </ul>
    471      */
    472     public void setRatingType(int type) {
    473         try {
    474             mBinder.setRatingType(type);
    475         } catch (RemoteException e) {
    476             Log.e(TAG, "Error in setRatingType.", e);
    477         }
    478     }
    479 
    480     /**
    481      * Set some extras that can be associated with the {@link MediaSession}. No assumptions should
    482      * be made as to how a {@link MediaController} will handle these extras.
    483      * Keys should be fully qualified (e.g. com.example.MY_EXTRA) to avoid conflicts.
    484      *
    485      * @param extras The extras associated with the {@link MediaSession}.
    486      */
    487     public void setExtras(@Nullable Bundle extras) {
    488         try {
    489             mBinder.setExtras(extras);
    490         } catch (RemoteException e) {
    491             Log.wtf("Dead object in setExtras.", e);
    492         }
    493     }
    494 
    495     /**
    496      * Notify the system that the remote volume changed.
    497      *
    498      * @param provider The provider that is handling volume changes.
    499      * @hide
    500      */
    501     public void notifyRemoteVolumeChanged(VolumeProvider provider) {
    502         synchronized (mLock) {
    503             if (provider == null || provider != mVolumeProvider) {
    504                 Log.w(TAG, "Received update from stale volume provider");
    505                 return;
    506             }
    507         }
    508         try {
    509             mBinder.setCurrentVolume(provider.getCurrentVolume());
    510         } catch (RemoteException e) {
    511             Log.e(TAG, "Error in notifyVolumeChanged", e);
    512         }
    513     }
    514 
    515     private void dispatchPlay() {
    516         postToCallback(CallbackMessageHandler.MSG_PLAY);
    517     }
    518 
    519     private void dispatchPlayFromMediaId(String mediaId, Bundle extras) {
    520         postToCallback(CallbackMessageHandler.MSG_PLAY_MEDIA_ID, mediaId, extras);
    521     }
    522 
    523     private void dispatchPlayFromSearch(String query, Bundle extras) {
    524         postToCallback(CallbackMessageHandler.MSG_PLAY_SEARCH, query, extras);
    525     }
    526 
    527     private void dispatchSkipToItem(long id) {
    528         postToCallback(CallbackMessageHandler.MSG_SKIP_TO_ITEM, id);
    529     }
    530 
    531     private void dispatchPause() {
    532         postToCallback(CallbackMessageHandler.MSG_PAUSE);
    533     }
    534 
    535     private void dispatchStop() {
    536         postToCallback(CallbackMessageHandler.MSG_STOP);
    537     }
    538 
    539     private void dispatchNext() {
    540         postToCallback(CallbackMessageHandler.MSG_NEXT);
    541     }
    542 
    543     private void dispatchPrevious() {
    544         postToCallback(CallbackMessageHandler.MSG_PREVIOUS);
    545     }
    546 
    547     private void dispatchFastForward() {
    548         postToCallback(CallbackMessageHandler.MSG_FAST_FORWARD);
    549     }
    550 
    551     private void dispatchRewind() {
    552         postToCallback(CallbackMessageHandler.MSG_REWIND);
    553     }
    554 
    555     private void dispatchSeekTo(long pos) {
    556         postToCallback(CallbackMessageHandler.MSG_SEEK_TO, pos);
    557     }
    558 
    559     private void dispatchRate(Rating rating) {
    560         postToCallback(CallbackMessageHandler.MSG_RATE, rating);
    561     }
    562 
    563     private void dispatchCustomAction(String action, Bundle args) {
    564         postToCallback(CallbackMessageHandler.MSG_CUSTOM_ACTION, action, args);
    565     }
    566 
    567     private void dispatchMediaButton(Intent mediaButtonIntent) {
    568         postToCallback(CallbackMessageHandler.MSG_MEDIA_BUTTON, mediaButtonIntent);
    569     }
    570 
    571     private void dispatchAdjustVolume(int direction) {
    572         postToCallback(CallbackMessageHandler.MSG_ADJUST_VOLUME, direction);
    573     }
    574 
    575     private void dispatchSetVolumeTo(int volume) {
    576         postToCallback(CallbackMessageHandler.MSG_SET_VOLUME, volume);
    577     }
    578 
    579     private void postToCallback(int what) {
    580         postToCallback(what, null);
    581     }
    582 
    583     private void postCommand(String command, Bundle args, ResultReceiver resultCb) {
    584         Command cmd = new Command(command, args, resultCb);
    585         postToCallback(CallbackMessageHandler.MSG_COMMAND, cmd);
    586     }
    587 
    588     private void postToCallback(int what, Object obj) {
    589         postToCallback(what, obj, null);
    590     }
    591 
    592     private void postToCallback(int what, Object obj, Bundle extras) {
    593         synchronized (mLock) {
    594             if (mCallback != null) {
    595                 mCallback.post(what, obj, extras);
    596             }
    597         }
    598     }
    599 
    600     /**
    601      * Return true if this is considered an active playback state.
    602      *
    603      * @hide
    604      */
    605     public static boolean isActiveState(int state) {
    606         switch (state) {
    607             case PlaybackState.STATE_FAST_FORWARDING:
    608             case PlaybackState.STATE_REWINDING:
    609             case PlaybackState.STATE_SKIPPING_TO_PREVIOUS:
    610             case PlaybackState.STATE_SKIPPING_TO_NEXT:
    611             case PlaybackState.STATE_BUFFERING:
    612             case PlaybackState.STATE_CONNECTING:
    613             case PlaybackState.STATE_PLAYING:
    614                 return true;
    615         }
    616         return false;
    617     }
    618 
    619     /**
    620      * Represents an ongoing session. This may be passed to apps by the session
    621      * owner to allow them to create a {@link MediaController} to communicate with
    622      * the session.
    623      */
    624     public static final class Token implements Parcelable {
    625 
    626         private ISessionController mBinder;
    627 
    628         /**
    629          * @hide
    630          */
    631         public Token(ISessionController binder) {
    632             mBinder = binder;
    633         }
    634 
    635         @Override
    636         public int describeContents() {
    637             return 0;
    638         }
    639 
    640         @Override
    641         public void writeToParcel(Parcel dest, int flags) {
    642             dest.writeStrongBinder(mBinder.asBinder());
    643         }
    644 
    645         @Override
    646         public int hashCode() {
    647             final int prime = 31;
    648             int result = 1;
    649             result = prime * result + ((mBinder == null) ? 0 : mBinder.asBinder().hashCode());
    650             return result;
    651         }
    652 
    653         @Override
    654         public boolean equals(Object obj) {
    655             if (this == obj)
    656                 return true;
    657             if (obj == null)
    658                 return false;
    659             if (getClass() != obj.getClass())
    660                 return false;
    661             Token other = (Token) obj;
    662             if (mBinder == null) {
    663                 if (other.mBinder != null)
    664                     return false;
    665             } else if (!mBinder.asBinder().equals(other.mBinder.asBinder()))
    666                 return false;
    667             return true;
    668         }
    669 
    670         ISessionController getBinder() {
    671             return mBinder;
    672         }
    673 
    674         public static final Parcelable.Creator<Token> CREATOR
    675                 = new Parcelable.Creator<Token>() {
    676             @Override
    677             public Token createFromParcel(Parcel in) {
    678                 return new Token(ISessionController.Stub.asInterface(in.readStrongBinder()));
    679             }
    680 
    681             @Override
    682             public Token[] newArray(int size) {
    683                 return new Token[size];
    684             }
    685         };
    686     }
    687 
    688     /**
    689      * Receives media buttons, transport controls, and commands from controllers
    690      * and the system. A callback may be set using {@link #setCallback}.
    691      */
    692     public abstract static class Callback {
    693         private MediaSession mSession;
    694 
    695         public Callback() {
    696         }
    697 
    698         /**
    699          * Called when a controller has sent a command to this session.
    700          * The owner of the session may handle custom commands but is not
    701          * required to.
    702          *
    703          * @param command The command name.
    704          * @param args Optional parameters for the command, may be null.
    705          * @param cb A result receiver to which a result may be sent by the command, may be null.
    706          */
    707         public void onCommand(@NonNull String command, @Nullable Bundle args,
    708                 @Nullable ResultReceiver cb) {
    709         }
    710 
    711         /**
    712          * Called when a media button is pressed and this session has the
    713          * highest priority or a controller sends a media button event to the
    714          * session. The default behavior will call the relevant method if the
    715          * action for it was set.
    716          * <p>
    717          * The intent will be of type {@link Intent#ACTION_MEDIA_BUTTON} with a
    718          * KeyEvent in {@link Intent#EXTRA_KEY_EVENT}
    719          *
    720          * @param mediaButtonIntent an intent containing the KeyEvent as an
    721          *            extra
    722          * @return True if the event was handled, false otherwise.
    723          */
    724         public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) {
    725             if (mSession != null
    726                     && Intent.ACTION_MEDIA_BUTTON.equals(mediaButtonIntent.getAction())) {
    727                 KeyEvent ke = mediaButtonIntent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
    728                 if (ke != null && ke.getAction() == KeyEvent.ACTION_DOWN) {
    729                     PlaybackState state = mSession.mPlaybackState;
    730                     long validActions = state == null ? 0 : state.getActions();
    731                     switch (ke.getKeyCode()) {
    732                         case KeyEvent.KEYCODE_MEDIA_PLAY:
    733                             if ((validActions & PlaybackState.ACTION_PLAY) != 0) {
    734                                 onPlay();
    735                                 return true;
    736                             }
    737                             break;
    738                         case KeyEvent.KEYCODE_MEDIA_PAUSE:
    739                             if ((validActions & PlaybackState.ACTION_PAUSE) != 0) {
    740                                 onPause();
    741                                 return true;
    742                             }
    743                             break;
    744                         case KeyEvent.KEYCODE_MEDIA_NEXT:
    745                             if ((validActions & PlaybackState.ACTION_SKIP_TO_NEXT) != 0) {
    746                                 onSkipToNext();
    747                                 return true;
    748                             }
    749                             break;
    750                         case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
    751                             if ((validActions & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0) {
    752                                 onSkipToPrevious();
    753                                 return true;
    754                             }
    755                             break;
    756                         case KeyEvent.KEYCODE_MEDIA_STOP:
    757                             if ((validActions & PlaybackState.ACTION_STOP) != 0) {
    758                                 onStop();
    759                                 return true;
    760                             }
    761                             break;
    762                         case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
    763                             if ((validActions & PlaybackState.ACTION_FAST_FORWARD) != 0) {
    764                                 onFastForward();
    765                                 return true;
    766                             }
    767                             break;
    768                         case KeyEvent.KEYCODE_MEDIA_REWIND:
    769                             if ((validActions & PlaybackState.ACTION_REWIND) != 0) {
    770                                 onRewind();
    771                                 return true;
    772                             }
    773                             break;
    774                         case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
    775                         case KeyEvent.KEYCODE_HEADSETHOOK:
    776                             boolean isPlaying = state == null ? false
    777                                     : state.getState() == PlaybackState.STATE_PLAYING;
    778                             boolean canPlay = (validActions & (PlaybackState.ACTION_PLAY_PAUSE
    779                                     | PlaybackState.ACTION_PLAY)) != 0;
    780                             boolean canPause = (validActions & (PlaybackState.ACTION_PLAY_PAUSE
    781                                     | PlaybackState.ACTION_PAUSE)) != 0;
    782                             if (isPlaying && canPause) {
    783                                 onPause();
    784                                 return true;
    785                             } else if (!isPlaying && canPlay) {
    786                                 onPlay();
    787                                 return true;
    788                             }
    789                             break;
    790                     }
    791                 }
    792             }
    793             return false;
    794         }
    795 
    796         /**
    797          * Override to handle requests to begin playback.
    798          */
    799         public void onPlay() {
    800         }
    801 
    802         /**
    803          * Override to handle requests to play a specific mediaId that was
    804          * provided by your app's {@link MediaBrowserService}.
    805          */
    806         public void onPlayFromMediaId(String mediaId, Bundle extras) {
    807         }
    808 
    809         /**
    810          * Override to handle requests to begin playback from a search query. An
    811          * empty query indicates that the app may play any music. The
    812          * implementation should attempt to make a smart choice about what to
    813          * play.
    814          */
    815         public void onPlayFromSearch(String query, Bundle extras) {
    816         }
    817 
    818         /**
    819          * Override to handle requests to play an item with a given id from the
    820          * play queue.
    821          */
    822         public void onSkipToQueueItem(long id) {
    823         }
    824 
    825         /**
    826          * Override to handle requests to pause playback.
    827          */
    828         public void onPause() {
    829         }
    830 
    831         /**
    832          * Override to handle requests to skip to the next media item.
    833          */
    834         public void onSkipToNext() {
    835         }
    836 
    837         /**
    838          * Override to handle requests to skip to the previous media item.
    839          */
    840         public void onSkipToPrevious() {
    841         }
    842 
    843         /**
    844          * Override to handle requests to fast forward.
    845          */
    846         public void onFastForward() {
    847         }
    848 
    849         /**
    850          * Override to handle requests to rewind.
    851          */
    852         public void onRewind() {
    853         }
    854 
    855         /**
    856          * Override to handle requests to stop playback.
    857          */
    858         public void onStop() {
    859         }
    860 
    861         /**
    862          * Override to handle requests to seek to a specific position in ms.
    863          *
    864          * @param pos New position to move to, in milliseconds.
    865          */
    866         public void onSeekTo(long pos) {
    867         }
    868 
    869         /**
    870          * Override to handle the item being rated.
    871          *
    872          * @param rating
    873          */
    874         public void onSetRating(@NonNull Rating rating) {
    875         }
    876 
    877         /**
    878          * Called when a {@link MediaController} wants a {@link PlaybackState.CustomAction} to be
    879          * performed.
    880          *
    881          * @param action The action that was originally sent in the
    882          *               {@link PlaybackState.CustomAction}.
    883          * @param extras Optional extras specified by the {@link MediaController}.
    884          */
    885         public void onCustomAction(@NonNull String action, @Nullable Bundle extras) {
    886         }
    887     }
    888 
    889     /**
    890      * @hide
    891      */
    892     public static class CallbackStub extends ISessionCallback.Stub {
    893         private WeakReference<MediaSession> mMediaSession;
    894 
    895         public CallbackStub(MediaSession session) {
    896             mMediaSession = new WeakReference<MediaSession>(session);
    897         }
    898 
    899         @Override
    900         public void onCommand(String command, Bundle args, ResultReceiver cb) {
    901             MediaSession session = mMediaSession.get();
    902             if (session != null) {
    903                 session.postCommand(command, args, cb);
    904             }
    905         }
    906 
    907         @Override
    908         public void onMediaButton(Intent mediaButtonIntent, int sequenceNumber,
    909                 ResultReceiver cb) {
    910             MediaSession session = mMediaSession.get();
    911             try {
    912                 if (session != null) {
    913                     session.dispatchMediaButton(mediaButtonIntent);
    914                 }
    915             } finally {
    916                 if (cb != null) {
    917                     cb.send(sequenceNumber, null);
    918                 }
    919             }
    920         }
    921 
    922         @Override
    923         public void onPlay() {
    924             MediaSession session = mMediaSession.get();
    925             if (session != null) {
    926                 session.dispatchPlay();
    927             }
    928         }
    929 
    930         @Override
    931         public void onPlayFromMediaId(String mediaId, Bundle extras) {
    932             MediaSession session = mMediaSession.get();
    933             if (session != null) {
    934                 session.dispatchPlayFromMediaId(mediaId, extras);
    935             }
    936         }
    937 
    938         @Override
    939         public void onPlayFromSearch(String query, Bundle extras) {
    940             MediaSession session = mMediaSession.get();
    941             if (session != null) {
    942                 session.dispatchPlayFromSearch(query, extras);
    943             }
    944         }
    945 
    946         @Override
    947         public void onSkipToTrack(long id) {
    948             MediaSession session = mMediaSession.get();
    949             if (session != null) {
    950                 session.dispatchSkipToItem(id);
    951             }
    952         }
    953 
    954         @Override
    955         public void onPause() {
    956             MediaSession session = mMediaSession.get();
    957             if (session != null) {
    958                 session.dispatchPause();
    959             }
    960         }
    961 
    962         @Override
    963         public void onStop() {
    964             MediaSession session = mMediaSession.get();
    965             if (session != null) {
    966                 session.dispatchStop();
    967             }
    968         }
    969 
    970         @Override
    971         public void onNext() {
    972             MediaSession session = mMediaSession.get();
    973             if (session != null) {
    974                 session.dispatchNext();
    975             }
    976         }
    977 
    978         @Override
    979         public void onPrevious() {
    980             MediaSession session = mMediaSession.get();
    981             if (session != null) {
    982                 session.dispatchPrevious();
    983             }
    984         }
    985 
    986         @Override
    987         public void onFastForward() {
    988             MediaSession session = mMediaSession.get();
    989             if (session != null) {
    990                 session.dispatchFastForward();
    991             }
    992         }
    993 
    994         @Override
    995         public void onRewind() {
    996             MediaSession session = mMediaSession.get();
    997             if (session != null) {
    998                 session.dispatchRewind();
    999             }
   1000         }
   1001 
   1002         @Override
   1003         public void onSeekTo(long pos) {
   1004             MediaSession session = mMediaSession.get();
   1005             if (session != null) {
   1006                 session.dispatchSeekTo(pos);
   1007             }
   1008         }
   1009 
   1010         @Override
   1011         public void onRate(Rating rating) {
   1012             MediaSession session = mMediaSession.get();
   1013             if (session != null) {
   1014                 session.dispatchRate(rating);
   1015             }
   1016         }
   1017 
   1018         @Override
   1019         public void onCustomAction(String action, Bundle args) {
   1020             MediaSession session = mMediaSession.get();
   1021             if (session != null) {
   1022                 session.dispatchCustomAction(action, args);
   1023             }
   1024         }
   1025 
   1026         @Override
   1027         public void onAdjustVolume(int direction) {
   1028             MediaSession session = mMediaSession.get();
   1029             if (session != null) {
   1030                 session.dispatchAdjustVolume(direction);
   1031             }
   1032         }
   1033 
   1034         @Override
   1035         public void onSetVolumeTo(int value) {
   1036             MediaSession session = mMediaSession.get();
   1037             if (session != null) {
   1038                 session.dispatchSetVolumeTo(value);
   1039             }
   1040         }
   1041 
   1042     }
   1043 
   1044     /**
   1045      * A single item that is part of the play queue. It contains a description
   1046      * of the item and its id in the queue.
   1047      */
   1048     public static final class QueueItem implements Parcelable {
   1049         /**
   1050          * This id is reserved. No items can be explicitly asigned this id.
   1051          */
   1052         public static final int UNKNOWN_ID = -1;
   1053 
   1054         private final MediaDescription mDescription;
   1055         private final long mId;
   1056 
   1057         /**
   1058          * Create a new {@link MediaSession.QueueItem}.
   1059          *
   1060          * @param description The {@link MediaDescription} for this item.
   1061          * @param id An identifier for this item. It must be unique within the
   1062          *            play queue and cannot be {@link #UNKNOWN_ID}.
   1063          */
   1064         public QueueItem(MediaDescription description, long id) {
   1065             if (description == null) {
   1066                 throw new IllegalArgumentException("Description cannot be null.");
   1067             }
   1068             if (id == UNKNOWN_ID) {
   1069                 throw new IllegalArgumentException("Id cannot be QueueItem.UNKNOWN_ID");
   1070             }
   1071             mDescription = description;
   1072             mId = id;
   1073         }
   1074 
   1075         private QueueItem(Parcel in) {
   1076             mDescription = MediaDescription.CREATOR.createFromParcel(in);
   1077             mId = in.readLong();
   1078         }
   1079 
   1080         /**
   1081          * Get the description for this item.
   1082          */
   1083         public MediaDescription getDescription() {
   1084             return mDescription;
   1085         }
   1086 
   1087         /**
   1088          * Get the queue id for this item.
   1089          */
   1090         public long getQueueId() {
   1091             return mId;
   1092         }
   1093 
   1094         @Override
   1095         public void writeToParcel(Parcel dest, int flags) {
   1096             mDescription.writeToParcel(dest, flags);
   1097             dest.writeLong(mId);
   1098         }
   1099 
   1100         @Override
   1101         public int describeContents() {
   1102             return 0;
   1103         }
   1104 
   1105         public static final Creator<MediaSession.QueueItem> CREATOR = new Creator<MediaSession.QueueItem>() {
   1106 
   1107             @Override
   1108             public MediaSession.QueueItem createFromParcel(Parcel p) {
   1109                 return new MediaSession.QueueItem(p);
   1110             }
   1111 
   1112             @Override
   1113             public MediaSession.QueueItem[] newArray(int size) {
   1114                 return new MediaSession.QueueItem[size];
   1115             }
   1116         };
   1117 
   1118         @Override
   1119         public String toString() {
   1120             return "MediaSession.QueueItem {" +
   1121                     "Description=" + mDescription +
   1122                     ", Id=" + mId + " }";
   1123         }
   1124     }
   1125 
   1126     private static final class Command {
   1127         public final String command;
   1128         public final Bundle extras;
   1129         public final ResultReceiver stub;
   1130 
   1131         public Command(String command, Bundle extras, ResultReceiver stub) {
   1132             this.command = command;
   1133             this.extras = extras;
   1134             this.stub = stub;
   1135         }
   1136     }
   1137 
   1138     private class CallbackMessageHandler extends Handler {
   1139 
   1140         private static final int MSG_PLAY = 1;
   1141         private static final int MSG_PLAY_MEDIA_ID = 2;
   1142         private static final int MSG_PLAY_SEARCH = 3;
   1143         private static final int MSG_SKIP_TO_ITEM = 4;
   1144         private static final int MSG_PAUSE = 5;
   1145         private static final int MSG_STOP = 6;
   1146         private static final int MSG_NEXT = 7;
   1147         private static final int MSG_PREVIOUS = 8;
   1148         private static final int MSG_FAST_FORWARD = 9;
   1149         private static final int MSG_REWIND = 10;
   1150         private static final int MSG_SEEK_TO = 11;
   1151         private static final int MSG_RATE = 12;
   1152         private static final int MSG_CUSTOM_ACTION = 13;
   1153         private static final int MSG_MEDIA_BUTTON = 14;
   1154         private static final int MSG_COMMAND = 15;
   1155         private static final int MSG_ADJUST_VOLUME = 16;
   1156         private static final int MSG_SET_VOLUME = 17;
   1157 
   1158         private MediaSession.Callback mCallback;
   1159 
   1160         public CallbackMessageHandler(Looper looper, MediaSession.Callback callback) {
   1161             super(looper, null, true);
   1162             mCallback = callback;
   1163         }
   1164 
   1165         public void post(int what, Object obj, Bundle bundle) {
   1166             Message msg = obtainMessage(what, obj);
   1167             msg.setData(bundle);
   1168             msg.sendToTarget();
   1169         }
   1170 
   1171         public void post(int what, Object obj) {
   1172             obtainMessage(what, obj).sendToTarget();
   1173         }
   1174 
   1175         public void post(int what) {
   1176             post(what, null);
   1177         }
   1178 
   1179         public void post(int what, Object obj, int arg1) {
   1180             obtainMessage(what, arg1, 0, obj).sendToTarget();
   1181         }
   1182 
   1183         @Override
   1184         public void handleMessage(Message msg) {
   1185             VolumeProvider vp;
   1186             switch (msg.what) {
   1187                 case MSG_PLAY:
   1188                     mCallback.onPlay();
   1189                     break;
   1190                 case MSG_PLAY_MEDIA_ID:
   1191                     mCallback.onPlayFromMediaId((String) msg.obj, msg.getData());
   1192                     break;
   1193                 case MSG_PLAY_SEARCH:
   1194                     mCallback.onPlayFromSearch((String) msg.obj, msg.getData());
   1195                     break;
   1196                 case MSG_SKIP_TO_ITEM:
   1197                     mCallback.onSkipToQueueItem((Long) msg.obj);
   1198                     break;
   1199                 case MSG_PAUSE:
   1200                     mCallback.onPause();
   1201                     break;
   1202                 case MSG_STOP:
   1203                     mCallback.onStop();
   1204                     break;
   1205                 case MSG_NEXT:
   1206                     mCallback.onSkipToNext();
   1207                     break;
   1208                 case MSG_PREVIOUS:
   1209                     mCallback.onSkipToPrevious();
   1210                     break;
   1211                 case MSG_FAST_FORWARD:
   1212                     mCallback.onFastForward();
   1213                     break;
   1214                 case MSG_REWIND:
   1215                     mCallback.onRewind();
   1216                     break;
   1217                 case MSG_SEEK_TO:
   1218                     mCallback.onSeekTo((Long) msg.obj);
   1219                     break;
   1220                 case MSG_RATE:
   1221                     mCallback.onSetRating((Rating) msg.obj);
   1222                     break;
   1223                 case MSG_CUSTOM_ACTION:
   1224                     mCallback.onCustomAction((String) msg.obj, msg.getData());
   1225                     break;
   1226                 case MSG_MEDIA_BUTTON:
   1227                     mCallback.onMediaButtonEvent((Intent) msg.obj);
   1228                     break;
   1229                 case MSG_COMMAND:
   1230                     Command cmd = (Command) msg.obj;
   1231                     mCallback.onCommand(cmd.command, cmd.extras, cmd.stub);
   1232                     break;
   1233                 case MSG_ADJUST_VOLUME:
   1234                     synchronized (mLock) {
   1235                         vp = mVolumeProvider;
   1236                     }
   1237                     if (vp != null) {
   1238                         vp.onAdjustVolume((int) msg.obj);
   1239                     }
   1240                     break;
   1241                 case MSG_SET_VOLUME:
   1242                     synchronized (mLock) {
   1243                         vp = mVolumeProvider;
   1244                     }
   1245                     if (vp != null) {
   1246                         vp.onSetVolumeTo((int) msg.obj);
   1247                     }
   1248                     break;
   1249             }
   1250         }
   1251     }
   1252 }
   1253