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