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 
    453         if (!mCbRegistered) {
    454             try {
    455                 mSessionBinder.registerCallbackListener(mCbStub);
    456                 mCbRegistered = true;
    457             } catch (RemoteException e) {
    458                 Log.e(TAG, "Dead object in registerCallback", e);
    459             }
    460         }
    461     }
    462 
    463     private boolean removeCallbackLocked(Callback cb) {
    464         boolean success = false;
    465         for (int i = mCallbacks.size() - 1; i >= 0; i--) {
    466             MessageHandler handler = mCallbacks.get(i);
    467             if (cb == handler.mCallback) {
    468                 mCallbacks.remove(i);
    469                 success = true;
    470             }
    471         }
    472         if (mCbRegistered && mCallbacks.size() == 0) {
    473             try {
    474                 mSessionBinder.unregisterCallbackListener(mCbStub);
    475             } catch (RemoteException e) {
    476                 Log.e(TAG, "Dead object in removeCallbackLocked");
    477             }
    478             mCbRegistered = false;
    479         }
    480         return success;
    481     }
    482 
    483     private MessageHandler getHandlerForCallbackLocked(Callback cb) {
    484         if (cb == null) {
    485             throw new IllegalArgumentException("Callback cannot be null");
    486         }
    487         for (int i = mCallbacks.size() - 1; i >= 0; i--) {
    488             MessageHandler handler = mCallbacks.get(i);
    489             if (cb == handler.mCallback) {
    490                 return handler;
    491             }
    492         }
    493         return null;
    494     }
    495 
    496     private final void postMessage(int what, Object obj, Bundle data) {
    497         synchronized (mLock) {
    498             for (int i = mCallbacks.size() - 1; i >= 0; i--) {
    499                 mCallbacks.get(i).post(what, obj, data);
    500             }
    501         }
    502     }
    503 
    504     /**
    505      * Callback for receiving updates on from the session. A Callback can be
    506      * registered using {@link #registerCallback}
    507      */
    508     public static abstract class Callback {
    509         /**
    510          * Override to handle the session being destroyed. The session is no
    511          * longer valid after this call and calls to it will be ignored.
    512          */
    513         public void onSessionDestroyed() {
    514         }
    515 
    516         /**
    517          * Override to handle custom events sent by the session owner without a
    518          * specified interface. Controllers should only handle these for
    519          * sessions they own.
    520          *
    521          * @param event The event from the session.
    522          * @param extras Optional parameters for the event, may be null.
    523          */
    524         public void onSessionEvent(@NonNull String event, @Nullable Bundle extras) {
    525         }
    526 
    527         /**
    528          * Override to handle changes in playback state.
    529          *
    530          * @param state The new playback state of the session
    531          */
    532         public void onPlaybackStateChanged(@NonNull PlaybackState state) {
    533         }
    534 
    535         /**
    536          * Override to handle changes to the current metadata.
    537          *
    538          * @param metadata The current metadata for the session or null if none.
    539          * @see MediaMetadata
    540          */
    541         public void onMetadataChanged(@Nullable MediaMetadata metadata) {
    542         }
    543 
    544         /**
    545          * Override to handle changes to items in the queue.
    546          *
    547          * @param queue A list of items in the current play queue. It should
    548          *            include the currently playing item as well as previous and
    549          *            upcoming items if applicable.
    550          * @see MediaSession.QueueItem
    551          */
    552         public void onQueueChanged(@Nullable List<MediaSession.QueueItem> queue) {
    553         }
    554 
    555         /**
    556          * Override to handle changes to the queue title.
    557          *
    558          * @param title The title that should be displayed along with the play queue such as
    559          *              "Now Playing". May be null if there is no such title.
    560          */
    561         public void onQueueTitleChanged(@Nullable CharSequence title) {
    562         }
    563 
    564         /**
    565          * Override to handle changes to the {@link MediaSession} extras.
    566          *
    567          * @param extras The extras that can include other information associated with the
    568          *               {@link MediaSession}.
    569          */
    570         public void onExtrasChanged(@Nullable Bundle extras) {
    571         }
    572 
    573         /**
    574          * Override to handle changes to the audio info.
    575          *
    576          * @param info The current audio info for this session.
    577          */
    578         public void onAudioInfoChanged(PlaybackInfo info) {
    579         }
    580     }
    581 
    582     /**
    583      * Interface for controlling media playback on a session. This allows an app
    584      * to send media transport commands to the session.
    585      */
    586     public final class TransportControls {
    587         private static final String TAG = "TransportController";
    588 
    589         private TransportControls() {
    590         }
    591 
    592         /**
    593          * Request that the player start its playback at its current position.
    594          */
    595         public void play() {
    596             try {
    597                 mSessionBinder.play();
    598             } catch (RemoteException e) {
    599                 Log.wtf(TAG, "Error calling play.", e);
    600             }
    601         }
    602 
    603         /**
    604          * Request that the player start playback for a specific {@link Uri}.
    605          *
    606          * @param mediaId The uri of the requested media.
    607          * @param extras Optional extras that can include extra information about the media item
    608          *               to be played.
    609          */
    610         public void playFromMediaId(String mediaId, Bundle extras) {
    611             if (TextUtils.isEmpty(mediaId)) {
    612                 throw new IllegalArgumentException(
    613                         "You must specify a non-empty String for playFromMediaId.");
    614             }
    615             try {
    616                 mSessionBinder.playFromMediaId(mediaId, extras);
    617             } catch (RemoteException e) {
    618                 Log.wtf(TAG, "Error calling play(" + mediaId + ").", e);
    619             }
    620         }
    621 
    622         /**
    623          * Request that the player start playback for a specific search query.
    624          * An empty or null query should be treated as a request to play any
    625          * music.
    626          *
    627          * @param query The search query.
    628          * @param extras Optional extras that can include extra information
    629          *            about the query.
    630          */
    631         public void playFromSearch(String query, Bundle extras) {
    632             if (query == null) {
    633                 // This is to remain compatible with
    634                 // INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH
    635                 query = "";
    636             }
    637             try {
    638                 mSessionBinder.playFromSearch(query, extras);
    639             } catch (RemoteException e) {
    640                 Log.wtf(TAG, "Error calling play(" + query + ").", e);
    641             }
    642         }
    643 
    644         /**
    645          * Play an item with a specific id in the play queue. If you specify an
    646          * id that is not in the play queue, the behavior is undefined.
    647          */
    648         public void skipToQueueItem(long id) {
    649             try {
    650                 mSessionBinder.skipToQueueItem(id);
    651             } catch (RemoteException e) {
    652                 Log.wtf(TAG, "Error calling skipToItem(" + id + ").", e);
    653             }
    654         }
    655 
    656         /**
    657          * Request that the player pause its playback and stay at its current
    658          * position.
    659          */
    660         public void pause() {
    661             try {
    662                 mSessionBinder.pause();
    663             } catch (RemoteException e) {
    664                 Log.wtf(TAG, "Error calling pause.", e);
    665             }
    666         }
    667 
    668         /**
    669          * Request that the player stop its playback; it may clear its state in
    670          * whatever way is appropriate.
    671          */
    672         public void stop() {
    673             try {
    674                 mSessionBinder.stop();
    675             } catch (RemoteException e) {
    676                 Log.wtf(TAG, "Error calling stop.", e);
    677             }
    678         }
    679 
    680         /**
    681          * Move to a new location in the media stream.
    682          *
    683          * @param pos Position to move to, in milliseconds.
    684          */
    685         public void seekTo(long pos) {
    686             try {
    687                 mSessionBinder.seekTo(pos);
    688             } catch (RemoteException e) {
    689                 Log.wtf(TAG, "Error calling seekTo.", e);
    690             }
    691         }
    692 
    693         /**
    694          * Start fast forwarding. If playback is already fast forwarding this
    695          * may increase the rate.
    696          */
    697         public void fastForward() {
    698             try {
    699                 mSessionBinder.fastForward();
    700             } catch (RemoteException e) {
    701                 Log.wtf(TAG, "Error calling fastForward.", e);
    702             }
    703         }
    704 
    705         /**
    706          * Skip to the next item.
    707          */
    708         public void skipToNext() {
    709             try {
    710                 mSessionBinder.next();
    711             } catch (RemoteException e) {
    712                 Log.wtf(TAG, "Error calling next.", e);
    713             }
    714         }
    715 
    716         /**
    717          * Start rewinding. If playback is already rewinding this may increase
    718          * the rate.
    719          */
    720         public void rewind() {
    721             try {
    722                 mSessionBinder.rewind();
    723             } catch (RemoteException e) {
    724                 Log.wtf(TAG, "Error calling rewind.", e);
    725             }
    726         }
    727 
    728         /**
    729          * Skip to the previous item.
    730          */
    731         public void skipToPrevious() {
    732             try {
    733                 mSessionBinder.previous();
    734             } catch (RemoteException e) {
    735                 Log.wtf(TAG, "Error calling previous.", e);
    736             }
    737         }
    738 
    739         /**
    740          * Rate the current content. This will cause the rating to be set for
    741          * the current user. The Rating type must match the type returned by
    742          * {@link #getRatingType()}.
    743          *
    744          * @param rating The rating to set for the current content
    745          */
    746         public void setRating(Rating rating) {
    747             try {
    748                 mSessionBinder.rate(rating);
    749             } catch (RemoteException e) {
    750                 Log.wtf(TAG, "Error calling rate.", e);
    751             }
    752         }
    753 
    754         /**
    755          * Send a custom action back for the {@link MediaSession} to perform.
    756          *
    757          * @param customAction The action to perform.
    758          * @param args Optional arguments to supply to the {@link MediaSession} for this
    759          *             custom action.
    760          */
    761         public void sendCustomAction(@NonNull PlaybackState.CustomAction customAction,
    762                     @Nullable Bundle args) {
    763             if (customAction == null) {
    764                 throw new IllegalArgumentException("CustomAction cannot be null.");
    765             }
    766             sendCustomAction(customAction.getAction(), args);
    767         }
    768 
    769         /**
    770          * Send the id and args from a custom action back for the {@link MediaSession} to perform.
    771          *
    772          * @see #sendCustomAction(PlaybackState.CustomAction action, Bundle args)
    773          * @param action The action identifier of the {@link PlaybackState.CustomAction} as
    774          *               specified by the {@link MediaSession}.
    775          * @param args Optional arguments to supply to the {@link MediaSession} for this
    776          *             custom action.
    777          */
    778         public void sendCustomAction(@NonNull String action, @Nullable Bundle args) {
    779             if (TextUtils.isEmpty(action)) {
    780                 throw new IllegalArgumentException("CustomAction cannot be null.");
    781             }
    782             try {
    783                 mSessionBinder.sendCustomAction(action, args);
    784             } catch (RemoteException e) {
    785                 Log.d(TAG, "Dead object in sendCustomAction.", e);
    786             }
    787         }
    788     }
    789 
    790     /**
    791      * Holds information about the current playback and how audio is handled for
    792      * this session.
    793      */
    794     public static final class PlaybackInfo {
    795         /**
    796          * The session uses remote playback.
    797          */
    798         public static final int PLAYBACK_TYPE_REMOTE = 2;
    799         /**
    800          * The session uses local playback.
    801          */
    802         public static final int PLAYBACK_TYPE_LOCAL = 1;
    803 
    804         private final int mVolumeType;
    805         private final int mVolumeControl;
    806         private final int mMaxVolume;
    807         private final int mCurrentVolume;
    808         private final AudioAttributes mAudioAttrs;
    809 
    810         /**
    811          * @hide
    812          */
    813         public PlaybackInfo(int type, AudioAttributes attrs, int control, int max, int current) {
    814             mVolumeType = type;
    815             mAudioAttrs = attrs;
    816             mVolumeControl = control;
    817             mMaxVolume = max;
    818             mCurrentVolume = current;
    819         }
    820 
    821         /**
    822          * Get the type of playback which affects volume handling. One of:
    823          * <ul>
    824          * <li>{@link #PLAYBACK_TYPE_LOCAL}</li>
    825          * <li>{@link #PLAYBACK_TYPE_REMOTE}</li>
    826          * </ul>
    827          *
    828          * @return The type of playback this session is using.
    829          */
    830         public int getPlaybackType() {
    831             return mVolumeType;
    832         }
    833 
    834         /**
    835          * Get the audio attributes for this session. The attributes will affect
    836          * volume handling for the session. When the volume type is
    837          * {@link PlaybackInfo#PLAYBACK_TYPE_REMOTE} these may be ignored by the
    838          * remote volume handler.
    839          *
    840          * @return The attributes for this session.
    841          */
    842         public AudioAttributes getAudioAttributes() {
    843             return mAudioAttrs;
    844         }
    845 
    846         /**
    847          * Get the type of volume control that can be used. One of:
    848          * <ul>
    849          * <li>{@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}</li>
    850          * <li>{@link VolumeProvider#VOLUME_CONTROL_RELATIVE}</li>
    851          * <li>{@link VolumeProvider#VOLUME_CONTROL_FIXED}</li>
    852          * </ul>
    853          *
    854          * @return The type of volume control that may be used with this
    855          *         session.
    856          */
    857         public int getVolumeControl() {
    858             return mVolumeControl;
    859         }
    860 
    861         /**
    862          * Get the maximum volume that may be set for this session.
    863          *
    864          * @return The maximum allowed volume where this session is playing.
    865          */
    866         public int getMaxVolume() {
    867             return mMaxVolume;
    868         }
    869 
    870         /**
    871          * Get the current volume for this session.
    872          *
    873          * @return The current volume where this session is playing.
    874          */
    875         public int getCurrentVolume() {
    876             return mCurrentVolume;
    877         }
    878     }
    879 
    880     private final static class CallbackStub extends ISessionControllerCallback.Stub {
    881         private final WeakReference<MediaController> mController;
    882 
    883         public CallbackStub(MediaController controller) {
    884             mController = new WeakReference<MediaController>(controller);
    885         }
    886 
    887         @Override
    888         public void onSessionDestroyed() {
    889             MediaController controller = mController.get();
    890             if (controller != null) {
    891                 controller.postMessage(MSG_DESTROYED, null, null);
    892             }
    893         }
    894 
    895         @Override
    896         public void onEvent(String event, Bundle extras) {
    897             MediaController controller = mController.get();
    898             if (controller != null) {
    899                 controller.postMessage(MSG_EVENT, event, extras);
    900             }
    901         }
    902 
    903         @Override
    904         public void onPlaybackStateChanged(PlaybackState state) {
    905             MediaController controller = mController.get();
    906             if (controller != null) {
    907                 controller.postMessage(MSG_UPDATE_PLAYBACK_STATE, state, null);
    908             }
    909         }
    910 
    911         @Override
    912         public void onMetadataChanged(MediaMetadata metadata) {
    913             MediaController controller = mController.get();
    914             if (controller != null) {
    915                 controller.postMessage(MSG_UPDATE_METADATA, metadata, null);
    916             }
    917         }
    918 
    919         @Override
    920         public void onQueueChanged(ParceledListSlice parceledQueue) {
    921             List<MediaSession.QueueItem> queue = parceledQueue == null ? null : parceledQueue
    922                     .getList();
    923             MediaController controller = mController.get();
    924             if (controller != null) {
    925                 controller.postMessage(MSG_UPDATE_QUEUE, queue, null);
    926             }
    927         }
    928 
    929         @Override
    930         public void onQueueTitleChanged(CharSequence title) {
    931             MediaController controller = mController.get();
    932             if (controller != null) {
    933                 controller.postMessage(MSG_UPDATE_QUEUE_TITLE, title, null);
    934             }
    935         }
    936 
    937         @Override
    938         public void onExtrasChanged(Bundle extras) {
    939             MediaController controller = mController.get();
    940             if (controller != null) {
    941                 controller.postMessage(MSG_UPDATE_EXTRAS, extras, null);
    942             }
    943         }
    944 
    945         @Override
    946         public void onVolumeInfoChanged(ParcelableVolumeInfo pvi) {
    947             MediaController controller = mController.get();
    948             if (controller != null) {
    949                 PlaybackInfo info = new PlaybackInfo(pvi.volumeType, pvi.audioAttrs, pvi.controlType,
    950                         pvi.maxVolume, pvi.currentVolume);
    951                 controller.postMessage(MSG_UPDATE_VOLUME, info, null);
    952             }
    953         }
    954 
    955     }
    956 
    957     private final static class MessageHandler extends Handler {
    958         private final MediaController.Callback mCallback;
    959 
    960         public MessageHandler(Looper looper, MediaController.Callback cb) {
    961             super(looper, null, true);
    962             mCallback = cb;
    963         }
    964 
    965         @Override
    966         public void handleMessage(Message msg) {
    967             switch (msg.what) {
    968                 case MSG_EVENT:
    969                     mCallback.onSessionEvent((String) msg.obj, msg.getData());
    970                     break;
    971                 case MSG_UPDATE_PLAYBACK_STATE:
    972                     mCallback.onPlaybackStateChanged((PlaybackState) msg.obj);
    973                     break;
    974                 case MSG_UPDATE_METADATA:
    975                     mCallback.onMetadataChanged((MediaMetadata) msg.obj);
    976                     break;
    977                 case MSG_UPDATE_QUEUE:
    978                     mCallback.onQueueChanged((List<MediaSession.QueueItem>) msg.obj);
    979                     break;
    980                 case MSG_UPDATE_QUEUE_TITLE:
    981                     mCallback.onQueueTitleChanged((CharSequence) msg.obj);
    982                     break;
    983                 case MSG_UPDATE_EXTRAS:
    984                     mCallback.onExtrasChanged((Bundle) msg.obj);
    985                     break;
    986                 case MSG_UPDATE_VOLUME:
    987                     mCallback.onAudioInfoChanged((PlaybackInfo) msg.obj);
    988                     break;
    989                 case MSG_DESTROYED:
    990                     mCallback.onSessionDestroyed();
    991                     break;
    992             }
    993         }
    994 
    995         public void post(int what, Object obj, Bundle data) {
    996             obtainMessage(what, obj).sendToTarget();
    997         }
    998     }
    999 
   1000 }
   1001