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.NonNull;
     20 import android.annotation.Nullable;
     21 import android.app.PendingIntent;
     22 import android.content.Context;
     23 import android.content.pm.ParceledListSlice;
     24 import android.media.AudioAttributes;
     25 import android.media.AudioManager;
     26 import android.media.MediaMetadata;
     27 import android.media.Rating;
     28 import android.media.VolumeProvider;
     29 import android.net.Uri;
     30 import android.os.Bundle;
     31 import android.os.Handler;
     32 import android.os.Looper;
     33 import android.os.Message;
     34 import android.os.RemoteException;
     35 import android.os.ResultReceiver;
     36 import android.text.TextUtils;
     37 import android.util.Log;
     38 import android.view.KeyEvent;
     39 
     40 import java.lang.ref.WeakReference;
     41 import java.util.ArrayList;
     42 import java.util.List;
     43 
     44 /**
     45  * Allows an app to interact with an ongoing media session. Media buttons and
     46  * other commands can be sent to the session. A callback may be registered to
     47  * receive updates from the session, such as metadata and play state changes.
     48  * <p>
     49  * A MediaController can be created through {@link MediaSessionManager} if you
     50  * hold the "android.permission.MEDIA_CONTENT_CONTROL" permission or are an
     51  * enabled notification listener or by getting a {@link MediaSession.Token}
     52  * directly from the session owner.
     53  * <p>
     54  * MediaController objects are thread-safe.
     55  */
     56 public final class MediaController {
     57     private static final String TAG = "MediaController";
     58 
     59     private static final int MSG_EVENT = 1;
     60     private static final int MSG_UPDATE_PLAYBACK_STATE = 2;
     61     private static final int MSG_UPDATE_METADATA = 3;
     62     private static final int MSG_UPDATE_VOLUME = 4;
     63     private static final int MSG_UPDATE_QUEUE = 5;
     64     private static final int MSG_UPDATE_QUEUE_TITLE = 6;
     65     private static final int MSG_UPDATE_EXTRAS = 7;
     66     private static final int MSG_DESTROYED = 8;
     67 
     68     private final ISessionController mSessionBinder;
     69 
     70     private final MediaSession.Token mToken;
     71     private final Context mContext;
     72     private final CallbackStub mCbStub = new CallbackStub(this);
     73     private final ArrayList<MessageHandler> mCallbacks = new ArrayList<MessageHandler>();
     74     private final Object mLock = new Object();
     75 
     76     private boolean mCbRegistered = false;
     77     private String mPackageName;
     78     private String mTag;
     79 
     80     private final TransportControls mTransportControls;
     81 
     82     /**
     83      * Call for creating a MediaController directly from a binder. Should only
     84      * be used by framework code.
     85      *
     86      * @hide
     87      */
     88     public MediaController(Context context, ISessionController sessionBinder) {
     89         if (sessionBinder == null) {
     90             throw new IllegalArgumentException("Session token cannot be null");
     91         }
     92         if (context == null) {
     93             throw new IllegalArgumentException("Context cannot be null");
     94         }
     95         mSessionBinder = sessionBinder;
     96         mTransportControls = new TransportControls();
     97         mToken = new MediaSession.Token(sessionBinder);
     98         mContext = context;
     99     }
    100 
    101     /**
    102      * Create a new MediaController from a session's token.
    103      *
    104      * @param context The caller's context.
    105      * @param token The token for the session.
    106      */
    107     public MediaController(@NonNull Context context, @NonNull MediaSession.Token token) {
    108         this(context, token.getBinder());
    109     }
    110 
    111     /**
    112      * Get a {@link TransportControls} instance to send transport actions to
    113      * the associated session.
    114      *
    115      * @return A transport controls instance.
    116      */
    117     public @NonNull TransportControls getTransportControls() {
    118         return mTransportControls;
    119     }
    120 
    121     /**
    122      * Send the specified media button event to the session. Only media keys can
    123      * be sent by this method, other keys will be ignored.
    124      *
    125      * @param keyEvent The media button event to dispatch.
    126      * @return true if the event was sent to the session, false otherwise.
    127      */
    128     public boolean dispatchMediaButtonEvent(@NonNull KeyEvent keyEvent) {
    129         if (keyEvent == null) {
    130             throw new IllegalArgumentException("KeyEvent may not be null");
    131         }
    132         if (!KeyEvent.isMediaKey(keyEvent.getKeyCode())) {
    133             return false;
    134         }
    135         try {
    136             return mSessionBinder.sendMediaButton(keyEvent);
    137         } catch (RemoteException e) {
    138             // System is dead. =(
    139         }
    140         return false;
    141     }
    142 
    143     /**
    144      * Get the current playback state for this session.
    145      *
    146      * @return The current PlaybackState or null
    147      */
    148     public @Nullable PlaybackState getPlaybackState() {
    149         try {
    150             return mSessionBinder.getPlaybackState();
    151         } catch (RemoteException e) {
    152             Log.wtf(TAG, "Error calling getPlaybackState.", e);
    153             return null;
    154         }
    155     }
    156 
    157     /**
    158      * Get the current metadata for this session.
    159      *
    160      * @return The current MediaMetadata or null.
    161      */
    162     public @Nullable MediaMetadata getMetadata() {
    163         try {
    164             return mSessionBinder.getMetadata();
    165         } catch (RemoteException e) {
    166             Log.wtf(TAG, "Error calling getMetadata.", e);
    167             return null;
    168         }
    169     }
    170 
    171     /**
    172      * Get the current play queue for this session if one is set. If you only
    173      * care about the current item {@link #getMetadata()} should be used.
    174      *
    175      * @return The current play queue or null.
    176      */
    177     public @Nullable List<MediaSession.QueueItem> getQueue() {
    178         try {
    179             ParceledListSlice queue = mSessionBinder.getQueue();
    180             if (queue != null) {
    181                 return queue.getList();
    182             }
    183         } catch (RemoteException e) {
    184             Log.wtf(TAG, "Error calling getQueue.", e);
    185         }
    186         return null;
    187     }
    188 
    189     /**
    190      * Get the queue title for this session.
    191      */
    192     public @Nullable CharSequence getQueueTitle() {
    193         try {
    194             return mSessionBinder.getQueueTitle();
    195         } catch (RemoteException e) {
    196             Log.wtf(TAG, "Error calling getQueueTitle", e);
    197         }
    198         return null;
    199     }
    200 
    201     /**
    202      * Get the extras for this session.
    203      */
    204     public @Nullable Bundle getExtras() {
    205         try {
    206             return mSessionBinder.getExtras();
    207         } catch (RemoteException e) {
    208             Log.wtf(TAG, "Error calling getExtras", e);
    209         }
    210         return null;
    211     }
    212 
    213     /**
    214      * Get the rating type supported by the session. One of:
    215      * <ul>
    216      * <li>{@link Rating#RATING_NONE}</li>
    217      * <li>{@link Rating#RATING_HEART}</li>
    218      * <li>{@link Rating#RATING_THUMB_UP_DOWN}</li>
    219      * <li>{@link Rating#RATING_3_STARS}</li>
    220      * <li>{@link Rating#RATING_4_STARS}</li>
    221      * <li>{@link Rating#RATING_5_STARS}</li>
    222      * <li>{@link Rating#RATING_PERCENTAGE}</li>
    223      * </ul>
    224      *
    225      * @return The supported rating type
    226      */
    227     public int getRatingType() {
    228         try {
    229             return mSessionBinder.getRatingType();
    230         } catch (RemoteException e) {
    231             Log.wtf(TAG, "Error calling getRatingType.", e);
    232             return Rating.RATING_NONE;
    233         }
    234     }
    235 
    236     /**
    237      * Get the flags for this session. Flags are defined in {@link MediaSession}.
    238      *
    239      * @return The current set of flags for the session.
    240      */
    241     public @MediaSession.SessionFlags long getFlags() {
    242         try {
    243             return mSessionBinder.getFlags();
    244         } catch (RemoteException e) {
    245             Log.wtf(TAG, "Error calling getFlags.", e);
    246         }
    247         return 0;
    248     }
    249 
    250     /**
    251      * Get the current playback info for this session.
    252      *
    253      * @return The current playback info or null.
    254      */
    255     public @Nullable PlaybackInfo getPlaybackInfo() {
    256         try {
    257             ParcelableVolumeInfo result = mSessionBinder.getVolumeAttributes();
    258             return new PlaybackInfo(result.volumeType, result.audioAttrs, result.controlType,
    259                     result.maxVolume, result.currentVolume);
    260 
    261         } catch (RemoteException e) {
    262             Log.wtf(TAG, "Error calling getAudioInfo.", e);
    263         }
    264         return null;
    265     }
    266 
    267     /**
    268      * Get an intent for launching UI associated with this session if one
    269      * exists.
    270      *
    271      * @return A {@link PendingIntent} to launch UI or null.
    272      */
    273     public @Nullable PendingIntent getSessionActivity() {
    274         try {
    275             return mSessionBinder.getLaunchPendingIntent();
    276         } catch (RemoteException e) {
    277             Log.wtf(TAG, "Error calling getPendingIntent.", e);
    278         }
    279         return null;
    280     }
    281 
    282     /**
    283      * Get the token for the session this is connected to.
    284      *
    285      * @return The token for the connected session.
    286      */
    287     public @NonNull MediaSession.Token getSessionToken() {
    288         return mToken;
    289     }
    290 
    291     /**
    292      * Set the volume of the output this session is playing on. The command will
    293      * be ignored if it does not support
    294      * {@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}. The flags in
    295      * {@link AudioManager} may be used to affect the handling.
    296      *
    297      * @see #getPlaybackInfo()
    298      * @param value The value to set it to, between 0 and the reported max.
    299      * @param flags Flags from {@link AudioManager} to include with the volume
    300      *            request.
    301      */
    302     public void setVolumeTo(int value, int flags) {
    303         try {
    304             mSessionBinder.setVolumeTo(value, flags, mContext.getPackageName());
    305         } catch (RemoteException e) {
    306             Log.wtf(TAG, "Error calling setVolumeTo.", e);
    307         }
    308     }
    309 
    310     /**
    311      * Adjust the volume of the output this session is playing on. The direction
    312      * must be one of {@link AudioManager#ADJUST_LOWER},
    313      * {@link AudioManager#ADJUST_RAISE}, or {@link AudioManager#ADJUST_SAME}.
    314      * The command will be ignored if the session does not support
    315      * {@link VolumeProvider#VOLUME_CONTROL_RELATIVE} or
    316      * {@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}. The flags in
    317      * {@link AudioManager} may be used to affect the handling.
    318      *
    319      * @see #getPlaybackInfo()
    320      * @param direction The direction to adjust the volume in.
    321      * @param flags Any flags to pass with the command.
    322      */
    323     public void adjustVolume(int direction, int flags) {
    324         try {
    325             mSessionBinder.adjustVolume(direction, flags, mContext.getPackageName());
    326         } catch (RemoteException e) {
    327             Log.wtf(TAG, "Error calling adjustVolumeBy.", e);
    328         }
    329     }
    330 
    331     /**
    332      * Registers a callback to receive updates from the Session. Updates will be
    333      * posted on the caller's thread.
    334      *
    335      * @param callback The callback object, must not be null.
    336      */
    337     public void registerCallback(@NonNull Callback callback) {
    338         registerCallback(callback, null);
    339     }
    340 
    341     /**
    342      * Registers a callback to receive updates from the session. Updates will be
    343      * posted on the specified handler's thread.
    344      *
    345      * @param callback The callback object, must not be null.
    346      * @param handler The handler to post updates on. If null the callers thread
    347      *            will be used.
    348      */
    349     public void registerCallback(@NonNull Callback callback, @Nullable Handler handler) {
    350         if (callback == null) {
    351             throw new IllegalArgumentException("callback must not be null");
    352         }
    353         if (handler == null) {
    354             handler = new Handler();
    355         }
    356         synchronized (mLock) {
    357             addCallbackLocked(callback, handler);
    358         }
    359     }
    360 
    361     /**
    362      * Unregisters the specified callback. If an update has already been posted
    363      * you may still receive it after calling this method.
    364      *
    365      * @param callback The callback to remove.
    366      */
    367     public void unregisterCallback(@NonNull Callback callback) {
    368         if (callback == null) {
    369             throw new IllegalArgumentException("callback must not be null");
    370         }
    371         synchronized (mLock) {
    372             removeCallbackLocked(callback);
    373         }
    374     }
    375 
    376     /**
    377      * Sends a generic command to the session. It is up to the session creator
    378      * to decide what commands and parameters they will support. As such,
    379      * commands should only be sent to sessions that the controller owns.
    380      *
    381      * @param command The command to send
    382      * @param args Any parameters to include with the command
    383      * @param cb The callback to receive the result on
    384      */
    385     public void sendCommand(@NonNull String command, @Nullable Bundle args,
    386             @Nullable ResultReceiver cb) {
    387         if (TextUtils.isEmpty(command)) {
    388             throw new IllegalArgumentException("command cannot be null or empty");
    389         }
    390         try {
    391             mSessionBinder.sendCommand(command, args, cb);
    392         } catch (RemoteException e) {
    393             Log.d(TAG, "Dead object in sendCommand.", e);
    394         }
    395     }
    396 
    397     /**
    398      * Get the session owner's package name.
    399      *
    400      * @return The package name of of the session owner.
    401      */
    402     public String getPackageName() {
    403         if (mPackageName == null) {
    404             try {
    405                 mPackageName = mSessionBinder.getPackageName();
    406             } catch (RemoteException e) {
    407                 Log.d(TAG, "Dead object in getPackageName.", e);
    408             }
    409         }
    410         return mPackageName;
    411     }
    412 
    413     /**
    414      * Get the session's tag for debugging purposes.
    415      *
    416      * @return The session's tag.
    417      * @hide
    418      */
    419     public String getTag() {
    420         if (mTag == null) {
    421             try {
    422                 mTag = mSessionBinder.getTag();
    423             } catch (RemoteException e) {
    424                 Log.d(TAG, "Dead object in getTag.", e);
    425             }
    426         }
    427         return mTag;
    428     }
    429 
    430     /*
    431      * @hide
    432      */
    433     ISessionController getSessionBinder() {
    434         return mSessionBinder;
    435     }
    436 
    437     /**
    438      * @hide
    439      */
    440     public boolean controlsSameSession(MediaController other) {
    441         if (other == null) return false;
    442         return mSessionBinder.asBinder() == other.getSessionBinder().asBinder();
    443     }
    444 
    445     private void addCallbackLocked(Callback cb, Handler handler) {
    446         if (getHandlerForCallbackLocked(cb) != null) {
    447             Log.w(TAG, "Callback is already added, ignoring");
    448             return;
    449         }
    450         MessageHandler holder = new MessageHandler(handler.getLooper(), cb);
    451         mCallbacks.add(holder);
    452         holder.mRegistered = true;
    453 
    454         if (!mCbRegistered) {
    455             try {
    456                 mSessionBinder.registerCallbackListener(mCbStub);
    457                 mCbRegistered = true;
    458             } catch (RemoteException e) {
    459                 Log.e(TAG, "Dead object in registerCallback", e);
    460             }
    461         }
    462     }
    463 
    464     private boolean removeCallbackLocked(Callback cb) {
    465         boolean success = false;
    466         for (int i = mCallbacks.size() - 1; i >= 0; i--) {
    467             MessageHandler handler = mCallbacks.get(i);
    468             if (cb == handler.mCallback) {
    469                 mCallbacks.remove(i);
    470                 success = true;
    471                 handler.mRegistered = false;
    472             }
    473         }
    474         if (mCbRegistered && mCallbacks.size() == 0) {
    475             try {
    476                 mSessionBinder.unregisterCallbackListener(mCbStub);
    477             } catch (RemoteException e) {
    478                 Log.e(TAG, "Dead object in removeCallbackLocked");
    479             }
    480             mCbRegistered = false;
    481         }
    482         return success;
    483     }
    484 
    485     private MessageHandler getHandlerForCallbackLocked(Callback cb) {
    486         if (cb == null) {
    487             throw new IllegalArgumentException("Callback cannot be null");
    488         }
    489         for (int i = mCallbacks.size() - 1; i >= 0; i--) {
    490             MessageHandler handler = mCallbacks.get(i);
    491             if (cb == handler.mCallback) {
    492                 return handler;
    493             }
    494         }
    495         return null;
    496     }
    497 
    498     private final void postMessage(int what, Object obj, Bundle data) {
    499         synchronized (mLock) {
    500             for (int i = mCallbacks.size() - 1; i >= 0; i--) {
    501                 mCallbacks.get(i).post(what, obj, data);
    502             }
    503         }
    504     }
    505 
    506     /**
    507      * Callback for receiving updates from the session. A Callback can be
    508      * registered using {@link #registerCallback}.
    509      */
    510     public static abstract class Callback {
    511         /**
    512          * Override to handle the session being destroyed. The session is no
    513          * longer valid after this call and calls to it will be ignored.
    514          */
    515         public void onSessionDestroyed() {
    516         }
    517 
    518         /**
    519          * Override to handle custom events sent by the session owner without a
    520          * specified interface. Controllers should only handle these for
    521          * sessions they own.
    522          *
    523          * @param event The event from the session.
    524          * @param extras Optional parameters for the event, may be null.
    525          */
    526         public void onSessionEvent(@NonNull String event, @Nullable Bundle extras) {
    527         }
    528 
    529         /**
    530          * Override to handle changes in playback state.
    531          *
    532          * @param state The new playback state of the session
    533          */
    534         public void onPlaybackStateChanged(@NonNull PlaybackState state) {
    535         }
    536 
    537         /**
    538          * Override to handle changes to the current metadata.
    539          *
    540          * @param metadata The current metadata for the session or null if none.
    541          * @see MediaMetadata
    542          */
    543         public void onMetadataChanged(@Nullable MediaMetadata metadata) {
    544         }
    545 
    546         /**
    547          * Override to handle changes to items in the queue.
    548          *
    549          * @param queue A list of items in the current play queue. It should
    550          *            include the currently playing item as well as previous and
    551          *            upcoming items if applicable.
    552          * @see MediaSession.QueueItem
    553          */
    554         public void onQueueChanged(@Nullable List<MediaSession.QueueItem> queue) {
    555         }
    556 
    557         /**
    558          * Override to handle changes to the queue title.
    559          *
    560          * @param title The title that should be displayed along with the play queue such as
    561          *              "Now Playing". May be null if there is no such title.
    562          */
    563         public void onQueueTitleChanged(@Nullable CharSequence title) {
    564         }
    565 
    566         /**
    567          * Override to handle changes to the {@link MediaSession} extras.
    568          *
    569          * @param extras The extras that can include other information associated with the
    570          *               {@link MediaSession}.
    571          */
    572         public void onExtrasChanged(@Nullable Bundle extras) {
    573         }
    574 
    575         /**
    576          * Override to handle changes to the audio info.
    577          *
    578          * @param info The current audio info for this session.
    579          */
    580         public void onAudioInfoChanged(PlaybackInfo info) {
    581         }
    582     }
    583 
    584     /**
    585      * Interface for controlling media playback on a session. This allows an app
    586      * to send media transport commands to the session.
    587      */
    588     public final class TransportControls {
    589         private static final String TAG = "TransportController";
    590 
    591         private TransportControls() {
    592         }
    593 
    594         /**
    595          * Request that the player prepare its playback. In other words, other sessions can continue
    596          * to play during the preparation of this session. This method can be used to speed up the
    597          * start of the playback. Once the preparation is done, the session will change its playback
    598          * state to {@link PlaybackState#STATE_PAUSED}. Afterwards, {@link #play} can be called to
    599          * start playback.
    600          */
    601         public void prepare() {
    602             try {
    603                 mSessionBinder.prepare();
    604             } catch (RemoteException e) {
    605                 Log.wtf(TAG, "Error calling prepare.", e);
    606             }
    607         }
    608 
    609         /**
    610          * Request that the player prepare playback for a specific media id. In other words, other
    611          * sessions can continue to play during the preparation of this session. This method can be
    612          * used to speed up the start of the playback. Once the preparation is done, the session
    613          * will change its playback state to {@link PlaybackState#STATE_PAUSED}. Afterwards,
    614          * {@link #play} can be called to start playback. If the preparation is not needed,
    615          * {@link #playFromMediaId} can be directly called without this method.
    616          *
    617          * @param mediaId The id of the requested media.
    618          * @param extras Optional extras that can include extra information about the media item
    619          *               to be prepared.
    620          */
    621         public void prepareFromMediaId(String mediaId, Bundle extras) {
    622             if (TextUtils.isEmpty(mediaId)) {
    623                 throw new IllegalArgumentException(
    624                         "You must specify a non-empty String for prepareFromMediaId.");
    625             }
    626             try {
    627                 mSessionBinder.prepareFromMediaId(mediaId, extras);
    628             } catch (RemoteException e) {
    629                 Log.wtf(TAG, "Error calling prepare(" + mediaId + ").", e);
    630             }
    631         }
    632 
    633         /**
    634          * Request that the player prepare playback for a specific search query. An empty or null
    635          * query should be treated as a request to prepare any music. In other words, other sessions
    636          * can continue to play during the preparation of this session. This method can be used to
    637          * speed up the start of the playback. Once the preparation is done, the session will
    638          * change its playback state to {@link PlaybackState#STATE_PAUSED}. Afterwards,
    639          * {@link #play} can be called to start playback. If the preparation is not needed,
    640          * {@link #playFromSearch} can be directly called without this method.
    641          *
    642          * @param query The search query.
    643          * @param extras Optional extras that can include extra information
    644          *               about the query.
    645          */
    646         public void prepareFromSearch(String query, Bundle extras) {
    647             if (query == null) {
    648                 // This is to remain compatible with
    649                 // INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH
    650                 query = "";
    651             }
    652             try {
    653                 mSessionBinder.prepareFromSearch(query, extras);
    654             } catch (RemoteException e) {
    655                 Log.wtf(TAG, "Error calling prepare(" + query + ").", e);
    656             }
    657         }
    658 
    659         /**
    660          * Request that the player prepare playback for a specific {@link Uri}. In other words,
    661          * other sessions can continue to play during the preparation of this session. This method
    662          * can be used to speed up the start of the playback. Once the preparation is done, the
    663          * session will change its playback state to {@link PlaybackState#STATE_PAUSED}. Afterwards,
    664          * {@link #play} can be called to start playback. If the preparation is not needed,
    665          * {@link #playFromUri} can be directly called without this method.
    666          *
    667          * @param uri The URI of the requested media.
    668          * @param extras Optional extras that can include extra information about the media item
    669          *               to be prepared.
    670          */
    671         public void prepareFromUri(Uri uri, Bundle extras) {
    672             if (uri == null || Uri.EMPTY.equals(uri)) {
    673                 throw new IllegalArgumentException(
    674                         "You must specify a non-empty Uri for prepareFromUri.");
    675             }
    676             try {
    677                 mSessionBinder.prepareFromUri(uri, extras);
    678             } catch (RemoteException e) {
    679                 Log.wtf(TAG, "Error calling prepare(" + uri + ").", e);
    680             }
    681         }
    682 
    683         /**
    684          * Request that the player start its playback at its current position.
    685          */
    686         public void play() {
    687             try {
    688                 mSessionBinder.play();
    689             } catch (RemoteException e) {
    690                 Log.wtf(TAG, "Error calling play.", e);
    691             }
    692         }
    693 
    694         /**
    695          * Request that the player start playback for a specific media id.
    696          *
    697          * @param mediaId The id of the requested media.
    698          * @param extras Optional extras that can include extra information about the media item
    699          *               to be played.
    700          */
    701         public void playFromMediaId(String mediaId, Bundle extras) {
    702             if (TextUtils.isEmpty(mediaId)) {
    703                 throw new IllegalArgumentException(
    704                         "You must specify a non-empty String for playFromMediaId.");
    705             }
    706             try {
    707                 mSessionBinder.playFromMediaId(mediaId, extras);
    708             } catch (RemoteException e) {
    709                 Log.wtf(TAG, "Error calling play(" + mediaId + ").", e);
    710             }
    711         }
    712 
    713         /**
    714          * Request that the player start playback for a specific search query.
    715          * An empty or null query should be treated as a request to play any
    716          * music.
    717          *
    718          * @param query The search query.
    719          * @param extras Optional extras that can include extra information
    720          *               about the query.
    721          */
    722         public void playFromSearch(String query, Bundle extras) {
    723             if (query == null) {
    724                 // This is to remain compatible with
    725                 // INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH
    726                 query = "";
    727             }
    728             try {
    729                 mSessionBinder.playFromSearch(query, extras);
    730             } catch (RemoteException e) {
    731                 Log.wtf(TAG, "Error calling play(" + query + ").", e);
    732             }
    733         }
    734 
    735         /**
    736          * Request that the player start playback for a specific {@link Uri}.
    737          *
    738          * @param uri The URI of the requested media.
    739          * @param extras Optional extras that can include extra information about the media item
    740          *               to be played.
    741          */
    742         public void playFromUri(Uri uri, Bundle extras) {
    743             if (uri == null || Uri.EMPTY.equals(uri)) {
    744                 throw new IllegalArgumentException(
    745                         "You must specify a non-empty Uri for playFromUri.");
    746             }
    747             try {
    748                 mSessionBinder.playFromUri(uri, extras);
    749             } catch (RemoteException e) {
    750                 Log.wtf(TAG, "Error calling play(" + uri + ").", e);
    751             }
    752         }
    753 
    754         /**
    755          * Play an item with a specific id in the play queue. If you specify an
    756          * id that is not in the play queue, the behavior is undefined.
    757          */
    758         public void skipToQueueItem(long id) {
    759             try {
    760                 mSessionBinder.skipToQueueItem(id);
    761             } catch (RemoteException e) {
    762                 Log.wtf(TAG, "Error calling skipToItem(" + id + ").", e);
    763             }
    764         }
    765 
    766         /**
    767          * Request that the player pause its playback and stay at its current
    768          * position.
    769          */
    770         public void pause() {
    771             try {
    772                 mSessionBinder.pause();
    773             } catch (RemoteException e) {
    774                 Log.wtf(TAG, "Error calling pause.", e);
    775             }
    776         }
    777 
    778         /**
    779          * Request that the player stop its playback; it may clear its state in
    780          * whatever way is appropriate.
    781          */
    782         public void stop() {
    783             try {
    784                 mSessionBinder.stop();
    785             } catch (RemoteException e) {
    786                 Log.wtf(TAG, "Error calling stop.", e);
    787             }
    788         }
    789 
    790         /**
    791          * Move to a new location in the media stream.
    792          *
    793          * @param pos Position to move to, in milliseconds.
    794          */
    795         public void seekTo(long pos) {
    796             try {
    797                 mSessionBinder.seekTo(pos);
    798             } catch (RemoteException e) {
    799                 Log.wtf(TAG, "Error calling seekTo.", e);
    800             }
    801         }
    802 
    803         /**
    804          * Start fast forwarding. If playback is already fast forwarding this
    805          * may increase the rate.
    806          */
    807         public void fastForward() {
    808             try {
    809                 mSessionBinder.fastForward();
    810             } catch (RemoteException e) {
    811                 Log.wtf(TAG, "Error calling fastForward.", e);
    812             }
    813         }
    814 
    815         /**
    816          * Skip to the next item.
    817          */
    818         public void skipToNext() {
    819             try {
    820                 mSessionBinder.next();
    821             } catch (RemoteException e) {
    822                 Log.wtf(TAG, "Error calling next.", e);
    823             }
    824         }
    825 
    826         /**
    827          * Start rewinding. If playback is already rewinding this may increase
    828          * the rate.
    829          */
    830         public void rewind() {
    831             try {
    832                 mSessionBinder.rewind();
    833             } catch (RemoteException e) {
    834                 Log.wtf(TAG, "Error calling rewind.", e);
    835             }
    836         }
    837 
    838         /**
    839          * Skip to the previous item.
    840          */
    841         public void skipToPrevious() {
    842             try {
    843                 mSessionBinder.previous();
    844             } catch (RemoteException e) {
    845                 Log.wtf(TAG, "Error calling previous.", e);
    846             }
    847         }
    848 
    849         /**
    850          * Rate the current content. This will cause the rating to be set for
    851          * the current user. The Rating type must match the type returned by
    852          * {@link #getRatingType()}.
    853          *
    854          * @param rating The rating to set for the current content
    855          */
    856         public void setRating(Rating rating) {
    857             try {
    858                 mSessionBinder.rate(rating);
    859             } catch (RemoteException e) {
    860                 Log.wtf(TAG, "Error calling rate.", e);
    861             }
    862         }
    863 
    864         /**
    865          * Send a custom action back for the {@link MediaSession} to perform.
    866          *
    867          * @param customAction The action to perform.
    868          * @param args Optional arguments to supply to the {@link MediaSession} for this
    869          *             custom action.
    870          */
    871         public void sendCustomAction(@NonNull PlaybackState.CustomAction customAction,
    872                     @Nullable Bundle args) {
    873             if (customAction == null) {
    874                 throw new IllegalArgumentException("CustomAction cannot be null.");
    875             }
    876             sendCustomAction(customAction.getAction(), args);
    877         }
    878 
    879         /**
    880          * Send the id and args from a custom action back for the {@link MediaSession} to perform.
    881          *
    882          * @see #sendCustomAction(PlaybackState.CustomAction action, Bundle args)
    883          * @param action The action identifier of the {@link PlaybackState.CustomAction} as
    884          *               specified by the {@link MediaSession}.
    885          * @param args Optional arguments to supply to the {@link MediaSession} for this
    886          *             custom action.
    887          */
    888         public void sendCustomAction(@NonNull String action, @Nullable Bundle args) {
    889             if (TextUtils.isEmpty(action)) {
    890                 throw new IllegalArgumentException("CustomAction cannot be null.");
    891             }
    892             try {
    893                 mSessionBinder.sendCustomAction(action, args);
    894             } catch (RemoteException e) {
    895                 Log.d(TAG, "Dead object in sendCustomAction.", e);
    896             }
    897         }
    898     }
    899 
    900     /**
    901      * Holds information about the current playback and how audio is handled for
    902      * this session.
    903      */
    904     public static final class PlaybackInfo {
    905         /**
    906          * The session uses remote playback.
    907          */
    908         public static final int PLAYBACK_TYPE_REMOTE = 2;
    909         /**
    910          * The session uses local playback.
    911          */
    912         public static final int PLAYBACK_TYPE_LOCAL = 1;
    913 
    914         private final int mVolumeType;
    915         private final int mVolumeControl;
    916         private final int mMaxVolume;
    917         private final int mCurrentVolume;
    918         private final AudioAttributes mAudioAttrs;
    919 
    920         /**
    921          * @hide
    922          */
    923         public PlaybackInfo(int type, AudioAttributes attrs, int control, int max, int current) {
    924             mVolumeType = type;
    925             mAudioAttrs = attrs;
    926             mVolumeControl = control;
    927             mMaxVolume = max;
    928             mCurrentVolume = current;
    929         }
    930 
    931         /**
    932          * Get the type of playback which affects volume handling. One of:
    933          * <ul>
    934          * <li>{@link #PLAYBACK_TYPE_LOCAL}</li>
    935          * <li>{@link #PLAYBACK_TYPE_REMOTE}</li>
    936          * </ul>
    937          *
    938          * @return The type of playback this session is using.
    939          */
    940         public int getPlaybackType() {
    941             return mVolumeType;
    942         }
    943 
    944         /**
    945          * Get the audio attributes for this session. The attributes will affect
    946          * volume handling for the session. When the volume type is
    947          * {@link PlaybackInfo#PLAYBACK_TYPE_REMOTE} these may be ignored by the
    948          * remote volume handler.
    949          *
    950          * @return The attributes for this session.
    951          */
    952         public AudioAttributes getAudioAttributes() {
    953             return mAudioAttrs;
    954         }
    955 
    956         /**
    957          * Get the type of volume control that can be used. One of:
    958          * <ul>
    959          * <li>{@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}</li>
    960          * <li>{@link VolumeProvider#VOLUME_CONTROL_RELATIVE}</li>
    961          * <li>{@link VolumeProvider#VOLUME_CONTROL_FIXED}</li>
    962          * </ul>
    963          *
    964          * @return The type of volume control that may be used with this
    965          *         session.
    966          */
    967         public int getVolumeControl() {
    968             return mVolumeControl;
    969         }
    970 
    971         /**
    972          * Get the maximum volume that may be set for this session.
    973          *
    974          * @return The maximum allowed volume where this session is playing.
    975          */
    976         public int getMaxVolume() {
    977             return mMaxVolume;
    978         }
    979 
    980         /**
    981          * Get the current volume for this session.
    982          *
    983          * @return The current volume where this session is playing.
    984          */
    985         public int getCurrentVolume() {
    986             return mCurrentVolume;
    987         }
    988     }
    989 
    990     private final static class CallbackStub extends ISessionControllerCallback.Stub {
    991         private final WeakReference<MediaController> mController;
    992 
    993         public CallbackStub(MediaController controller) {
    994             mController = new WeakReference<MediaController>(controller);
    995         }
    996 
    997         @Override
    998         public void onSessionDestroyed() {
    999             MediaController controller = mController.get();
   1000             if (controller != null) {
   1001                 controller.postMessage(MSG_DESTROYED, null, null);
   1002             }
   1003         }
   1004 
   1005         @Override
   1006         public void onEvent(String event, Bundle extras) {
   1007             MediaController controller = mController.get();
   1008             if (controller != null) {
   1009                 controller.postMessage(MSG_EVENT, event, extras);
   1010             }
   1011         }
   1012 
   1013         @Override
   1014         public void onPlaybackStateChanged(PlaybackState state) {
   1015             MediaController controller = mController.get();
   1016             if (controller != null) {
   1017                 controller.postMessage(MSG_UPDATE_PLAYBACK_STATE, state, null);
   1018             }
   1019         }
   1020 
   1021         @Override
   1022         public void onMetadataChanged(MediaMetadata metadata) {
   1023             MediaController controller = mController.get();
   1024             if (controller != null) {
   1025                 controller.postMessage(MSG_UPDATE_METADATA, metadata, null);
   1026             }
   1027         }
   1028 
   1029         @Override
   1030         public void onQueueChanged(ParceledListSlice parceledQueue) {
   1031             List<MediaSession.QueueItem> queue = parceledQueue == null ? null : parceledQueue
   1032                     .getList();
   1033             MediaController controller = mController.get();
   1034             if (controller != null) {
   1035                 controller.postMessage(MSG_UPDATE_QUEUE, queue, null);
   1036             }
   1037         }
   1038 
   1039         @Override
   1040         public void onQueueTitleChanged(CharSequence title) {
   1041             MediaController controller = mController.get();
   1042             if (controller != null) {
   1043                 controller.postMessage(MSG_UPDATE_QUEUE_TITLE, title, null);
   1044             }
   1045         }
   1046 
   1047         @Override
   1048         public void onExtrasChanged(Bundle extras) {
   1049             MediaController controller = mController.get();
   1050             if (controller != null) {
   1051                 controller.postMessage(MSG_UPDATE_EXTRAS, extras, null);
   1052             }
   1053         }
   1054 
   1055         @Override
   1056         public void onVolumeInfoChanged(ParcelableVolumeInfo pvi) {
   1057             MediaController controller = mController.get();
   1058             if (controller != null) {
   1059                 PlaybackInfo info = new PlaybackInfo(pvi.volumeType, pvi.audioAttrs, pvi.controlType,
   1060                         pvi.maxVolume, pvi.currentVolume);
   1061                 controller.postMessage(MSG_UPDATE_VOLUME, info, null);
   1062             }
   1063         }
   1064 
   1065     }
   1066 
   1067     private final static class MessageHandler extends Handler {
   1068         private final MediaController.Callback mCallback;
   1069         private boolean mRegistered = false;
   1070 
   1071         public MessageHandler(Looper looper, MediaController.Callback cb) {
   1072             super(looper, null, true);
   1073             mCallback = cb;
   1074         }
   1075 
   1076         @Override
   1077         public void handleMessage(Message msg) {
   1078             if (!mRegistered) {
   1079                 return;
   1080             }
   1081             switch (msg.what) {
   1082                 case MSG_EVENT:
   1083                     mCallback.onSessionEvent((String) msg.obj, msg.getData());
   1084                     break;
   1085                 case MSG_UPDATE_PLAYBACK_STATE:
   1086                     mCallback.onPlaybackStateChanged((PlaybackState) msg.obj);
   1087                     break;
   1088                 case MSG_UPDATE_METADATA:
   1089                     mCallback.onMetadataChanged((MediaMetadata) msg.obj);
   1090                     break;
   1091                 case MSG_UPDATE_QUEUE:
   1092                     mCallback.onQueueChanged((List<MediaSession.QueueItem>) msg.obj);
   1093                     break;
   1094                 case MSG_UPDATE_QUEUE_TITLE:
   1095                     mCallback.onQueueTitleChanged((CharSequence) msg.obj);
   1096                     break;
   1097                 case MSG_UPDATE_EXTRAS:
   1098                     mCallback.onExtrasChanged((Bundle) msg.obj);
   1099                     break;
   1100                 case MSG_UPDATE_VOLUME:
   1101                     mCallback.onAudioInfoChanged((PlaybackInfo) msg.obj);
   1102                     break;
   1103                 case MSG_DESTROYED:
   1104                     mCallback.onSessionDestroyed();
   1105                     break;
   1106             }
   1107         }
   1108 
   1109         public void post(int what, Object obj, Bundle data) {
   1110             Message msg = obtainMessage(what, obj);
   1111             msg.setData(data);
   1112             msg.sendToTarget();
   1113         }
   1114     }
   1115 
   1116 }
   1117