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