Home | History | Annotate | Download | only in media
      1 /*
      2  * Copyright 2018 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 package com.android.support.mediarouter.media;
     17 
     18 import android.app.PendingIntent;
     19 import android.content.BroadcastReceiver;
     20 import android.content.Context;
     21 import android.content.Intent;
     22 import android.content.IntentFilter;
     23 import android.net.Uri;
     24 import android.os.Bundle;
     25 import android.support.v4.util.ObjectsCompat;
     26 import android.util.Log;
     27 
     28 /**
     29  * A helper class for playing media on remote routes using the remote playback protocol
     30  * defined by {@link MediaControlIntent}.
     31  * <p>
     32  * The client maintains session state and offers a simplified interface for issuing
     33  * remote playback media control intents to a single route.
     34  * </p>
     35  */
     36 public class RemotePlaybackClient {
     37     static final String TAG = "RemotePlaybackClient";
     38     static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     39 
     40     private final Context mContext;
     41     private final MediaRouter.RouteInfo mRoute;
     42     private final ActionReceiver mActionReceiver;
     43     private final PendingIntent mItemStatusPendingIntent;
     44     private final PendingIntent mSessionStatusPendingIntent;
     45     private final PendingIntent mMessagePendingIntent;
     46 
     47     private boolean mRouteSupportsRemotePlayback;
     48     private boolean mRouteSupportsQueuing;
     49     private boolean mRouteSupportsSessionManagement;
     50     private boolean mRouteSupportsMessaging;
     51 
     52     String mSessionId;
     53     StatusCallback mStatusCallback;
     54     OnMessageReceivedListener mOnMessageReceivedListener;
     55 
     56     /**
     57      * Creates a remote playback client for a route.
     58      *
     59      * @param route The media route.
     60      */
     61     public RemotePlaybackClient(Context context, MediaRouter.RouteInfo route) {
     62         if (context == null) {
     63             throw new IllegalArgumentException("context must not be null");
     64         }
     65         if (route == null) {
     66             throw new IllegalArgumentException("route must not be null");
     67         }
     68 
     69         mContext = context;
     70         mRoute = route;
     71 
     72         IntentFilter actionFilter = new IntentFilter();
     73         actionFilter.addAction(ActionReceiver.ACTION_ITEM_STATUS_CHANGED);
     74         actionFilter.addAction(ActionReceiver.ACTION_SESSION_STATUS_CHANGED);
     75         actionFilter.addAction(ActionReceiver.ACTION_MESSAGE_RECEIVED);
     76         mActionReceiver = new ActionReceiver();
     77         context.registerReceiver(mActionReceiver, actionFilter);
     78 
     79         Intent itemStatusIntent = new Intent(ActionReceiver.ACTION_ITEM_STATUS_CHANGED);
     80         itemStatusIntent.setPackage(context.getPackageName());
     81         mItemStatusPendingIntent = PendingIntent.getBroadcast(
     82                 context, 0, itemStatusIntent, 0);
     83 
     84         Intent sessionStatusIntent = new Intent(ActionReceiver.ACTION_SESSION_STATUS_CHANGED);
     85         sessionStatusIntent.setPackage(context.getPackageName());
     86         mSessionStatusPendingIntent = PendingIntent.getBroadcast(
     87                 context, 0, sessionStatusIntent, 0);
     88 
     89         Intent messageIntent = new Intent(ActionReceiver.ACTION_MESSAGE_RECEIVED);
     90         messageIntent.setPackage(context.getPackageName());
     91         mMessagePendingIntent = PendingIntent.getBroadcast(
     92                 context, 0, messageIntent, 0);
     93         detectFeatures();
     94     }
     95 
     96     /**
     97      * Releases resources owned by the client.
     98      */
     99     public void release() {
    100         mContext.unregisterReceiver(mActionReceiver);
    101     }
    102 
    103     /**
    104      * Returns true if the route supports remote playback.
    105      * <p>
    106      * If the route does not support remote playback, then none of the functionality
    107      * offered by the client will be available.
    108      * </p><p>
    109      * This method returns true if the route supports all of the following
    110      * actions: {@link MediaControlIntent#ACTION_PLAY play},
    111      * {@link MediaControlIntent#ACTION_SEEK seek},
    112      * {@link MediaControlIntent#ACTION_GET_STATUS get status},
    113      * {@link MediaControlIntent#ACTION_PAUSE pause},
    114      * {@link MediaControlIntent#ACTION_RESUME resume},
    115      * {@link MediaControlIntent#ACTION_STOP stop}.
    116      * </p>
    117      *
    118      * @return True if remote playback is supported.
    119      */
    120     public boolean isRemotePlaybackSupported() {
    121         return mRouteSupportsRemotePlayback;
    122     }
    123 
    124     /**
    125      * Returns true if the route supports queuing features.
    126      * <p>
    127      * If the route does not support queuing, then at most one media item can be played
    128      * at a time and the {@link #enqueue} method will not be available.
    129      * </p><p>
    130      * This method returns true if the route supports all of the basic remote playback
    131      * actions and all of the following actions:
    132      * {@link MediaControlIntent#ACTION_ENQUEUE enqueue},
    133      * {@link MediaControlIntent#ACTION_REMOVE remove}.
    134      * </p>
    135      *
    136      * @return True if queuing is supported.  Implies {@link #isRemotePlaybackSupported}
    137      * is also true.
    138      *
    139      * @see #isRemotePlaybackSupported
    140      */
    141     public boolean isQueuingSupported() {
    142         return mRouteSupportsQueuing;
    143     }
    144 
    145     /**
    146      * Returns true if the route supports session management features.
    147      * <p>
    148      * If the route does not support session management, then the session will
    149      * not be created until the first media item is played.
    150      * </p><p>
    151      * This method returns true if the route supports all of the basic remote playback
    152      * actions and all of the following actions:
    153      * {@link MediaControlIntent#ACTION_START_SESSION start session},
    154      * {@link MediaControlIntent#ACTION_GET_SESSION_STATUS get session status},
    155      * {@link MediaControlIntent#ACTION_END_SESSION end session}.
    156      * </p>
    157      *
    158      * @return True if session management is supported.
    159      * Implies {@link #isRemotePlaybackSupported} is also true.
    160      *
    161      * @see #isRemotePlaybackSupported
    162      */
    163     public boolean isSessionManagementSupported() {
    164         return mRouteSupportsSessionManagement;
    165     }
    166 
    167     /**
    168      * Returns true if the route supports messages.
    169      * <p>
    170      * This method returns true if the route supports all of the basic remote playback
    171      * actions and all of the following actions:
    172      * {@link MediaControlIntent#ACTION_START_SESSION start session},
    173      * {@link MediaControlIntent#ACTION_SEND_MESSAGE send message},
    174      * {@link MediaControlIntent#ACTION_END_SESSION end session}.
    175      * </p>
    176      *
    177      * @return True if session management is supported.
    178      * Implies {@link #isRemotePlaybackSupported} is also true.
    179      *
    180      * @see #isRemotePlaybackSupported
    181      */
    182     public boolean isMessagingSupported() {
    183         return mRouteSupportsMessaging;
    184     }
    185 
    186     /**
    187      * Gets the current session id if there is one.
    188      *
    189      * @return The current session id, or null if none.
    190      */
    191     public String getSessionId() {
    192         return mSessionId;
    193     }
    194 
    195     /**
    196      * Sets the current session id.
    197      * <p>
    198      * It is usually not necessary to set the session id explicitly since
    199      * it is created as a side-effect of other requests such as
    200      * {@link #play}, {@link #enqueue}, and {@link #startSession}.
    201      * </p>
    202      *
    203      * @param sessionId The new session id, or null if none.
    204      */
    205     public void setSessionId(String sessionId) {
    206         if (!ObjectsCompat.equals(mSessionId, sessionId)) {
    207             if (DEBUG) {
    208                 Log.d(TAG, "Session id is now: " + sessionId);
    209             }
    210             mSessionId = sessionId;
    211             if (mStatusCallback != null) {
    212                 mStatusCallback.onSessionChanged(sessionId);
    213             }
    214         }
    215     }
    216 
    217     /**
    218      * Returns true if the client currently has a session.
    219      * <p>
    220      * Equivalent to checking whether {@link #getSessionId} returns a non-null result.
    221      * </p>
    222      *
    223      * @return True if there is a current session.
    224      */
    225     public boolean hasSession() {
    226         return mSessionId != null;
    227     }
    228 
    229     /**
    230      * Sets a callback that should receive status updates when the state of
    231      * media sessions or media items created by this instance of the remote
    232      * playback client changes.
    233      * <p>
    234      * The callback should be set before the session is created or any play
    235      * commands are issued.
    236      * </p>
    237      *
    238      * @param callback The callback to set.  May be null to remove the previous callback.
    239      */
    240     public void setStatusCallback(StatusCallback callback) {
    241         mStatusCallback = callback;
    242     }
    243 
    244     /**
    245      * Sets a callback that should receive messages when a message is sent from
    246      * media sessions created by this instance of the remote playback client changes.
    247      * <p>
    248      * The callback should be set before the session is created.
    249      * </p>
    250      *
    251      * @param listener The callback to set.  May be null to remove the previous callback.
    252      */
    253     public void setOnMessageReceivedListener(OnMessageReceivedListener listener) {
    254         mOnMessageReceivedListener = listener;
    255     }
    256 
    257     /**
    258      * Sends a request to play a media item.
    259      * <p>
    260      * Clears the queue and starts playing the new item immediately.  If the queue
    261      * was previously paused, then it is resumed as a side-effect of this request.
    262      * </p><p>
    263      * The request is issued in the current session.  If no session is available, then
    264      * one is created implicitly.
    265      * </p><p>
    266      * Please refer to {@link MediaControlIntent#ACTION_PLAY ACTION_PLAY} for
    267      * more information about the semantics of this request.
    268      * </p>
    269      *
    270      * @param contentUri The content Uri to play.
    271      * @param mimeType The mime type of the content, or null if unknown.
    272      * @param positionMillis The initial content position for the item in milliseconds,
    273      * or <code>0</code> to start at the beginning.
    274      * @param metadata The media item metadata bundle, or null if none.
    275      * @param extras A bundle of extra arguments to be added to the
    276      * {@link MediaControlIntent#ACTION_PLAY} intent, or null if none.
    277      * @param callback A callback to invoke when the request has been
    278      * processed, or null if none.
    279      *
    280      * @throws UnsupportedOperationException if the route does not support remote playback.
    281      *
    282      * @see MediaControlIntent#ACTION_PLAY
    283      * @see #isRemotePlaybackSupported
    284      */
    285     public void play(Uri contentUri, String mimeType, Bundle metadata,
    286             long positionMillis, Bundle extras, ItemActionCallback callback) {
    287         playOrEnqueue(contentUri, mimeType, metadata, positionMillis,
    288                 extras, callback, MediaControlIntent.ACTION_PLAY);
    289     }
    290 
    291     /**
    292      * Sends a request to enqueue a media item.
    293      * <p>
    294      * Enqueues a new item to play.  If the queue was previously paused, then will
    295      * remain paused.
    296      * </p><p>
    297      * The request is issued in the current session.  If no session is available, then
    298      * one is created implicitly.
    299      * </p><p>
    300      * Please refer to {@link MediaControlIntent#ACTION_ENQUEUE ACTION_ENQUEUE} for
    301      * more information about the semantics of this request.
    302      * </p>
    303      *
    304      * @param contentUri The content Uri to enqueue.
    305      * @param mimeType The mime type of the content, or null if unknown.
    306      * @param positionMillis The initial content position for the item in milliseconds,
    307      * or <code>0</code> to start at the beginning.
    308      * @param metadata The media item metadata bundle, or null if none.
    309      * @param extras A bundle of extra arguments to be added to the
    310      * {@link MediaControlIntent#ACTION_ENQUEUE} intent, or null if none.
    311      * @param callback A callback to invoke when the request has been
    312      * processed, or null if none.
    313      *
    314      * @throws UnsupportedOperationException if the route does not support queuing.
    315      *
    316      * @see MediaControlIntent#ACTION_ENQUEUE
    317      * @see #isRemotePlaybackSupported
    318      * @see #isQueuingSupported
    319      */
    320     public void enqueue(Uri contentUri, String mimeType, Bundle metadata,
    321             long positionMillis, Bundle extras, ItemActionCallback callback) {
    322         playOrEnqueue(contentUri, mimeType, metadata, positionMillis,
    323                 extras, callback, MediaControlIntent.ACTION_ENQUEUE);
    324     }
    325 
    326     private void playOrEnqueue(Uri contentUri, String mimeType, Bundle metadata,
    327             long positionMillis, Bundle extras,
    328             final ItemActionCallback callback, String action) {
    329         if (contentUri == null) {
    330             throw new IllegalArgumentException("contentUri must not be null");
    331         }
    332         throwIfRemotePlaybackNotSupported();
    333         if (action.equals(MediaControlIntent.ACTION_ENQUEUE)) {
    334             throwIfQueuingNotSupported();
    335         }
    336 
    337         Intent intent = new Intent(action);
    338         intent.setDataAndType(contentUri, mimeType);
    339         intent.putExtra(MediaControlIntent.EXTRA_ITEM_STATUS_UPDATE_RECEIVER,
    340                 mItemStatusPendingIntent);
    341         if (metadata != null) {
    342             intent.putExtra(MediaControlIntent.EXTRA_ITEM_METADATA, metadata);
    343         }
    344         if (positionMillis != 0) {
    345             intent.putExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, positionMillis);
    346         }
    347         performItemAction(intent, mSessionId, null, extras, callback);
    348     }
    349 
    350     /**
    351      * Sends a request to seek to a new position in a media item.
    352      * <p>
    353      * Seeks to a new position.  If the queue was previously paused then it
    354      * remains paused but the item's new position is still remembered.
    355      * </p><p>
    356      * The request is issued in the current session.
    357      * </p><p>
    358      * Please refer to {@link MediaControlIntent#ACTION_SEEK ACTION_SEEK} for
    359      * more information about the semantics of this request.
    360      * </p>
    361      *
    362      * @param itemId The item id.
    363      * @param positionMillis The new content position for the item in milliseconds,
    364      * or <code>0</code> to start at the beginning.
    365      * @param extras A bundle of extra arguments to be added to the
    366      * {@link MediaControlIntent#ACTION_SEEK} intent, or null if none.
    367      * @param callback A callback to invoke when the request has been
    368      * processed, or null if none.
    369      *
    370      * @throws IllegalStateException if there is no current session.
    371      *
    372      * @see MediaControlIntent#ACTION_SEEK
    373      * @see #isRemotePlaybackSupported
    374      */
    375     public void seek(String itemId, long positionMillis, Bundle extras,
    376             ItemActionCallback callback) {
    377         if (itemId == null) {
    378             throw new IllegalArgumentException("itemId must not be null");
    379         }
    380         throwIfNoCurrentSession();
    381 
    382         Intent intent = new Intent(MediaControlIntent.ACTION_SEEK);
    383         intent.putExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, positionMillis);
    384         performItemAction(intent, mSessionId, itemId, extras, callback);
    385     }
    386 
    387     /**
    388      * Sends a request to get the status of a media item.
    389      * <p>
    390      * The request is issued in the current session.
    391      * </p><p>
    392      * Please refer to {@link MediaControlIntent#ACTION_GET_STATUS ACTION_GET_STATUS} for
    393      * more information about the semantics of this request.
    394      * </p>
    395      *
    396      * @param itemId The item id.
    397      * @param extras A bundle of extra arguments to be added to the
    398      * {@link MediaControlIntent#ACTION_GET_STATUS} intent, or null if none.
    399      * @param callback A callback to invoke when the request has been
    400      * processed, or null if none.
    401      *
    402      * @throws IllegalStateException if there is no current session.
    403      *
    404      * @see MediaControlIntent#ACTION_GET_STATUS
    405      * @see #isRemotePlaybackSupported
    406      */
    407     public void getStatus(String itemId, Bundle extras, ItemActionCallback callback) {
    408         if (itemId == null) {
    409             throw new IllegalArgumentException("itemId must not be null");
    410         }
    411         throwIfNoCurrentSession();
    412 
    413         Intent intent = new Intent(MediaControlIntent.ACTION_GET_STATUS);
    414         performItemAction(intent, mSessionId, itemId, extras, callback);
    415     }
    416 
    417     /**
    418      * Sends a request to remove a media item from the queue.
    419      * <p>
    420      * The request is issued in the current session.
    421      * </p><p>
    422      * Please refer to {@link MediaControlIntent#ACTION_REMOVE ACTION_REMOVE} for
    423      * more information about the semantics of this request.
    424      * </p>
    425      *
    426      * @param itemId The item id.
    427      * @param extras A bundle of extra arguments to be added to the
    428      * {@link MediaControlIntent#ACTION_REMOVE} intent, or null if none.
    429      * @param callback A callback to invoke when the request has been
    430      * processed, or null if none.
    431      *
    432      * @throws IllegalStateException if there is no current session.
    433      * @throws UnsupportedOperationException if the route does not support queuing.
    434      *
    435      * @see MediaControlIntent#ACTION_REMOVE
    436      * @see #isRemotePlaybackSupported
    437      * @see #isQueuingSupported
    438      */
    439     public void remove(String itemId, Bundle extras, ItemActionCallback callback) {
    440         if (itemId == null) {
    441             throw new IllegalArgumentException("itemId must not be null");
    442         }
    443         throwIfQueuingNotSupported();
    444         throwIfNoCurrentSession();
    445 
    446         Intent intent = new Intent(MediaControlIntent.ACTION_REMOVE);
    447         performItemAction(intent, mSessionId, itemId, extras, callback);
    448     }
    449 
    450     /**
    451      * Sends a request to pause media playback.
    452      * <p>
    453      * The request is issued in the current session.  If playback is already paused
    454      * then the request has no effect.
    455      * </p><p>
    456      * Please refer to {@link MediaControlIntent#ACTION_PAUSE ACTION_PAUSE} for
    457      * more information about the semantics of this request.
    458      * </p>
    459      *
    460      * @param extras A bundle of extra arguments to be added to the
    461      * {@link MediaControlIntent#ACTION_PAUSE} intent, or null if none.
    462      * @param callback A callback to invoke when the request has been
    463      * processed, or null if none.
    464      *
    465      * @throws IllegalStateException if there is no current session.
    466      *
    467      * @see MediaControlIntent#ACTION_PAUSE
    468      * @see #isRemotePlaybackSupported
    469      */
    470     public void pause(Bundle extras, SessionActionCallback callback) {
    471         throwIfNoCurrentSession();
    472 
    473         Intent intent = new Intent(MediaControlIntent.ACTION_PAUSE);
    474         performSessionAction(intent, mSessionId, extras, callback);
    475     }
    476 
    477     /**
    478      * Sends a request to resume (unpause) media playback.
    479      * <p>
    480      * The request is issued in the current session.  If playback is not paused
    481      * then the request has no effect.
    482      * </p><p>
    483      * Please refer to {@link MediaControlIntent#ACTION_RESUME ACTION_RESUME} for
    484      * more information about the semantics of this request.
    485      * </p>
    486      *
    487      * @param extras A bundle of extra arguments to be added to the
    488      * {@link MediaControlIntent#ACTION_RESUME} intent, or null if none.
    489      * @param callback A callback to invoke when the request has been
    490      * processed, or null if none.
    491      *
    492      * @throws IllegalStateException if there is no current session.
    493      *
    494      * @see MediaControlIntent#ACTION_RESUME
    495      * @see #isRemotePlaybackSupported
    496      */
    497     public void resume(Bundle extras, SessionActionCallback callback) {
    498         throwIfNoCurrentSession();
    499 
    500         Intent intent = new Intent(MediaControlIntent.ACTION_RESUME);
    501         performSessionAction(intent, mSessionId, extras, callback);
    502     }
    503 
    504     /**
    505      * Sends a request to stop media playback and clear the media playback queue.
    506      * <p>
    507      * The request is issued in the current session.  If the queue is already
    508      * empty then the request has no effect.
    509      * </p><p>
    510      * Please refer to {@link MediaControlIntent#ACTION_STOP ACTION_STOP} for
    511      * more information about the semantics of this request.
    512      * </p>
    513      *
    514      * @param extras A bundle of extra arguments to be added to the
    515      * {@link MediaControlIntent#ACTION_STOP} intent, or null if none.
    516      * @param callback A callback to invoke when the request has been
    517      * processed, or null if none.
    518      *
    519      * @throws IllegalStateException if there is no current session.
    520      *
    521      * @see MediaControlIntent#ACTION_STOP
    522      * @see #isRemotePlaybackSupported
    523      */
    524     public void stop(Bundle extras, SessionActionCallback callback) {
    525         throwIfNoCurrentSession();
    526 
    527         Intent intent = new Intent(MediaControlIntent.ACTION_STOP);
    528         performSessionAction(intent, mSessionId, extras, callback);
    529     }
    530 
    531     /**
    532      * Sends a request to start a new media playback session.
    533      * <p>
    534      * The application must wait for the callback to indicate that this request
    535      * is complete before issuing other requests that affect the session.  If this
    536      * request is successful then the previous session will be invalidated.
    537      * </p><p>
    538      * Please refer to {@link MediaControlIntent#ACTION_START_SESSION ACTION_START_SESSION}
    539      * for more information about the semantics of this request.
    540      * </p>
    541      *
    542      * @param extras A bundle of extra arguments to be added to the
    543      * {@link MediaControlIntent#ACTION_START_SESSION} intent, or null if none.
    544      * @param callback A callback to invoke when the request has been
    545      * processed, or null if none.
    546      *
    547      * @throws UnsupportedOperationException if the route does not support session management.
    548      *
    549      * @see MediaControlIntent#ACTION_START_SESSION
    550      * @see #isRemotePlaybackSupported
    551      * @see #isSessionManagementSupported
    552      */
    553     public void startSession(Bundle extras, SessionActionCallback callback) {
    554         throwIfSessionManagementNotSupported();
    555 
    556         Intent intent = new Intent(MediaControlIntent.ACTION_START_SESSION);
    557         intent.putExtra(MediaControlIntent.EXTRA_SESSION_STATUS_UPDATE_RECEIVER,
    558                 mSessionStatusPendingIntent);
    559         if (mRouteSupportsMessaging) {
    560             intent.putExtra(MediaControlIntent.EXTRA_MESSAGE_RECEIVER, mMessagePendingIntent);
    561         }
    562         performSessionAction(intent, null, extras, callback);
    563     }
    564 
    565     /**
    566      * Sends a message.
    567      * <p>
    568      * The request is issued in the current session.
    569      * </p><p>
    570      * Please refer to {@link MediaControlIntent#ACTION_SEND_MESSAGE} for
    571      * more information about the semantics of this request.
    572      * </p>
    573      *
    574      * @param message A bundle message denoting {@link MediaControlIntent#EXTRA_MESSAGE}.
    575      * @param callback A callback to invoke when the request has been processed, or null if none.
    576      *
    577      * @throws IllegalStateException if there is no current session.
    578      * @throws UnsupportedOperationException if the route does not support messages.
    579      *
    580      * @see MediaControlIntent#ACTION_SEND_MESSAGE
    581      * @see #isMessagingSupported
    582      */
    583     public void sendMessage(Bundle message, SessionActionCallback callback) {
    584         throwIfNoCurrentSession();
    585         throwIfMessageNotSupported();
    586 
    587         Intent intent = new Intent(MediaControlIntent.ACTION_SEND_MESSAGE);
    588         performSessionAction(intent, mSessionId, message, callback);
    589     }
    590 
    591     /**
    592      * Sends a request to get the status of the media playback session.
    593      * <p>
    594      * The request is issued in the current session.
    595      * </p><p>
    596      * Please refer to {@link MediaControlIntent#ACTION_GET_SESSION_STATUS
    597      * ACTION_GET_SESSION_STATUS} for more information about the semantics of this request.
    598      * </p>
    599      *
    600      * @param extras A bundle of extra arguments to be added to the
    601      * {@link MediaControlIntent#ACTION_GET_SESSION_STATUS} intent, or null if none.
    602      * @param callback A callback to invoke when the request has been
    603      * processed, or null if none.
    604      *
    605      * @throws IllegalStateException if there is no current session.
    606      * @throws UnsupportedOperationException if the route does not support session management.
    607      *
    608      * @see MediaControlIntent#ACTION_GET_SESSION_STATUS
    609      * @see #isRemotePlaybackSupported
    610      * @see #isSessionManagementSupported
    611      */
    612     public void getSessionStatus(Bundle extras, SessionActionCallback callback) {
    613         throwIfSessionManagementNotSupported();
    614         throwIfNoCurrentSession();
    615 
    616         Intent intent = new Intent(MediaControlIntent.ACTION_GET_SESSION_STATUS);
    617         performSessionAction(intent, mSessionId, extras, callback);
    618     }
    619 
    620     /**
    621      * Sends a request to end the media playback session.
    622      * <p>
    623      * The request is issued in the current session.  If this request is successful,
    624      * the {@link #getSessionId session id property} will be set to null after
    625      * the callback is invoked.
    626      * </p><p>
    627      * Please refer to {@link MediaControlIntent#ACTION_END_SESSION ACTION_END_SESSION}
    628      * for more information about the semantics of this request.
    629      * </p>
    630      *
    631      * @param extras A bundle of extra arguments to be added to the
    632      * {@link MediaControlIntent#ACTION_END_SESSION} intent, or null if none.
    633      * @param callback A callback to invoke when the request has been
    634      * processed, or null if none.
    635      *
    636      * @throws IllegalStateException if there is no current session.
    637      * @throws UnsupportedOperationException if the route does not support session management.
    638      *
    639      * @see MediaControlIntent#ACTION_END_SESSION
    640      * @see #isRemotePlaybackSupported
    641      * @see #isSessionManagementSupported
    642      */
    643     public void endSession(Bundle extras, SessionActionCallback callback) {
    644         throwIfSessionManagementNotSupported();
    645         throwIfNoCurrentSession();
    646 
    647         Intent intent = new Intent(MediaControlIntent.ACTION_END_SESSION);
    648         performSessionAction(intent, mSessionId, extras, callback);
    649     }
    650 
    651     private void performItemAction(final Intent intent,
    652             final String sessionId, final String itemId,
    653             Bundle extras, final ItemActionCallback callback) {
    654         intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
    655         if (sessionId != null) {
    656             intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, sessionId);
    657         }
    658         if (itemId != null) {
    659             intent.putExtra(MediaControlIntent.EXTRA_ITEM_ID, itemId);
    660         }
    661         if (extras != null) {
    662             intent.putExtras(extras);
    663         }
    664         logRequest(intent);
    665         mRoute.sendControlRequest(intent, new MediaRouter.ControlRequestCallback() {
    666             @Override
    667             public void onResult(Bundle data) {
    668                 if (data != null) {
    669                     String sessionIdResult = inferMissingResult(sessionId,
    670                             data.getString(MediaControlIntent.EXTRA_SESSION_ID));
    671                     MediaSessionStatus sessionStatus = MediaSessionStatus.fromBundle(
    672                             data.getBundle(MediaControlIntent.EXTRA_SESSION_STATUS));
    673                     String itemIdResult = inferMissingResult(itemId,
    674                             data.getString(MediaControlIntent.EXTRA_ITEM_ID));
    675                     MediaItemStatus itemStatus = MediaItemStatus.fromBundle(
    676                             data.getBundle(MediaControlIntent.EXTRA_ITEM_STATUS));
    677                     adoptSession(sessionIdResult);
    678                     if (sessionIdResult != null && itemIdResult != null && itemStatus != null) {
    679                         if (DEBUG) {
    680                             Log.d(TAG, "Received result from " + intent.getAction()
    681                                     + ": data=" + bundleToString(data)
    682                                     + ", sessionId=" + sessionIdResult
    683                                     + ", sessionStatus=" + sessionStatus
    684                                     + ", itemId=" + itemIdResult
    685                                     + ", itemStatus=" + itemStatus);
    686                         }
    687                         callback.onResult(data, sessionIdResult, sessionStatus,
    688                                 itemIdResult, itemStatus);
    689                         return;
    690                     }
    691                 }
    692                 handleInvalidResult(intent, callback, data);
    693             }
    694 
    695             @Override
    696             public void onError(String error, Bundle data) {
    697                 handleError(intent, callback, error, data);
    698             }
    699         });
    700     }
    701 
    702     private void performSessionAction(final Intent intent, final String sessionId,
    703             Bundle extras, final SessionActionCallback callback) {
    704         intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
    705         if (sessionId != null) {
    706             intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, sessionId);
    707         }
    708         if (extras != null) {
    709             intent.putExtras(extras);
    710         }
    711         logRequest(intent);
    712         mRoute.sendControlRequest(intent, new MediaRouter.ControlRequestCallback() {
    713             @Override
    714             public void onResult(Bundle data) {
    715                 if (data != null) {
    716                     String sessionIdResult = inferMissingResult(sessionId,
    717                             data.getString(MediaControlIntent.EXTRA_SESSION_ID));
    718                     MediaSessionStatus sessionStatus = MediaSessionStatus.fromBundle(
    719                             data.getBundle(MediaControlIntent.EXTRA_SESSION_STATUS));
    720                     adoptSession(sessionIdResult);
    721                     if (sessionIdResult != null) {
    722                         if (DEBUG) {
    723                             Log.d(TAG, "Received result from " + intent.getAction()
    724                                     + ": data=" + bundleToString(data)
    725                                     + ", sessionId=" + sessionIdResult
    726                                     + ", sessionStatus=" + sessionStatus);
    727                         }
    728                         try {
    729                             callback.onResult(data, sessionIdResult, sessionStatus);
    730                         } finally {
    731                             if (intent.getAction().equals(MediaControlIntent.ACTION_END_SESSION)
    732                                     && sessionIdResult.equals(mSessionId)) {
    733                                 setSessionId(null);
    734                             }
    735                         }
    736                         return;
    737                     }
    738                 }
    739                 handleInvalidResult(intent, callback, data);
    740             }
    741 
    742             @Override
    743             public void onError(String error, Bundle data) {
    744                 handleError(intent, callback, error, data);
    745             }
    746         });
    747     }
    748 
    749     void adoptSession(String sessionId) {
    750         if (sessionId != null) {
    751             setSessionId(sessionId);
    752         }
    753     }
    754 
    755     void handleInvalidResult(Intent intent, ActionCallback callback,
    756             Bundle data) {
    757         Log.w(TAG, "Received invalid result data from " + intent.getAction()
    758                 + ": data=" + bundleToString(data));
    759         callback.onError(null, MediaControlIntent.ERROR_UNKNOWN, data);
    760     }
    761 
    762     void handleError(Intent intent, ActionCallback callback,
    763             String error, Bundle data) {
    764         final int code;
    765         if (data != null) {
    766             code = data.getInt(MediaControlIntent.EXTRA_ERROR_CODE,
    767                     MediaControlIntent.ERROR_UNKNOWN);
    768         } else {
    769             code = MediaControlIntent.ERROR_UNKNOWN;
    770         }
    771         if (DEBUG) {
    772             Log.w(TAG, "Received error from " + intent.getAction()
    773                     + ": error=" + error
    774                     + ", code=" + code
    775                     + ", data=" + bundleToString(data));
    776         }
    777         callback.onError(error, code, data);
    778     }
    779 
    780     private void detectFeatures() {
    781         mRouteSupportsRemotePlayback = routeSupportsAction(MediaControlIntent.ACTION_PLAY)
    782                 && routeSupportsAction(MediaControlIntent.ACTION_SEEK)
    783                 && routeSupportsAction(MediaControlIntent.ACTION_GET_STATUS)
    784                 && routeSupportsAction(MediaControlIntent.ACTION_PAUSE)
    785                 && routeSupportsAction(MediaControlIntent.ACTION_RESUME)
    786                 && routeSupportsAction(MediaControlIntent.ACTION_STOP);
    787         mRouteSupportsQueuing = mRouteSupportsRemotePlayback
    788                 && routeSupportsAction(MediaControlIntent.ACTION_ENQUEUE)
    789                 && routeSupportsAction(MediaControlIntent.ACTION_REMOVE);
    790         mRouteSupportsSessionManagement = mRouteSupportsRemotePlayback
    791                 && routeSupportsAction(MediaControlIntent.ACTION_START_SESSION)
    792                 && routeSupportsAction(MediaControlIntent.ACTION_GET_SESSION_STATUS)
    793                 && routeSupportsAction(MediaControlIntent.ACTION_END_SESSION);
    794         mRouteSupportsMessaging = doesRouteSupportMessaging();
    795     }
    796 
    797     private boolean routeSupportsAction(String action) {
    798         return mRoute.supportsControlAction(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK, action);
    799     }
    800 
    801     private boolean doesRouteSupportMessaging() {
    802         for (IntentFilter filter : mRoute.getControlFilters()) {
    803             if (filter.hasAction(MediaControlIntent.ACTION_SEND_MESSAGE)) {
    804                 return true;
    805             }
    806         }
    807         return false;
    808     }
    809 
    810     private void throwIfRemotePlaybackNotSupported() {
    811         if (!mRouteSupportsRemotePlayback) {
    812             throw new UnsupportedOperationException("The route does not support remote playback.");
    813         }
    814     }
    815 
    816     private void throwIfQueuingNotSupported() {
    817         if (!mRouteSupportsQueuing) {
    818             throw new UnsupportedOperationException("The route does not support queuing.");
    819         }
    820     }
    821 
    822     private void throwIfSessionManagementNotSupported() {
    823         if (!mRouteSupportsSessionManagement) {
    824             throw new UnsupportedOperationException("The route does not support "
    825                     + "session management.");
    826         }
    827     }
    828 
    829     private void throwIfMessageNotSupported() {
    830         if (!mRouteSupportsMessaging) {
    831             throw new UnsupportedOperationException("The route does not support message.");
    832         }
    833     }
    834 
    835     private void throwIfNoCurrentSession() {
    836         if (mSessionId == null) {
    837             throw new IllegalStateException("There is no current session.");
    838         }
    839     }
    840 
    841     static String inferMissingResult(String request, String result) {
    842         if (result == null) {
    843             // Result is missing.
    844             return request;
    845         }
    846         if (request == null || request.equals(result)) {
    847             // Request didn't specify a value or result matches request.
    848             return result;
    849         }
    850         // Result conflicts with request.
    851         return null;
    852     }
    853 
    854     private static void logRequest(Intent intent) {
    855         if (DEBUG) {
    856             Log.d(TAG, "Sending request: " + intent);
    857         }
    858     }
    859 
    860     static String bundleToString(Bundle bundle) {
    861         if (bundle != null) {
    862             bundle.size(); // force bundle to be unparcelled
    863             return bundle.toString();
    864         }
    865         return "null";
    866     }
    867 
    868     private final class ActionReceiver extends BroadcastReceiver {
    869         public static final String ACTION_ITEM_STATUS_CHANGED =
    870                 "android.support.v7.media.actions.ACTION_ITEM_STATUS_CHANGED";
    871         public static final String ACTION_SESSION_STATUS_CHANGED =
    872                 "android.support.v7.media.actions.ACTION_SESSION_STATUS_CHANGED";
    873         public static final String ACTION_MESSAGE_RECEIVED =
    874                 "android.support.v7.media.actions.ACTION_MESSAGE_RECEIVED";
    875 
    876         ActionReceiver() {
    877         }
    878 
    879         @Override
    880         public void onReceive(Context context, Intent intent) {
    881             String sessionId = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
    882             if (sessionId == null || !sessionId.equals(mSessionId)) {
    883                 Log.w(TAG, "Discarding spurious status callback "
    884                         + "with missing or invalid session id: sessionId=" + sessionId);
    885                 return;
    886             }
    887 
    888             MediaSessionStatus sessionStatus = MediaSessionStatus.fromBundle(
    889                     intent.getBundleExtra(MediaControlIntent.EXTRA_SESSION_STATUS));
    890             String action = intent.getAction();
    891             if (action.equals(ACTION_ITEM_STATUS_CHANGED)) {
    892                 String itemId = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID);
    893                 if (itemId == null) {
    894                     Log.w(TAG, "Discarding spurious status callback with missing item id.");
    895                     return;
    896                 }
    897 
    898                 MediaItemStatus itemStatus = MediaItemStatus.fromBundle(
    899                         intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_STATUS));
    900                 if (itemStatus == null) {
    901                     Log.w(TAG, "Discarding spurious status callback with missing item status.");
    902                     return;
    903                 }
    904 
    905                 if (DEBUG) {
    906                     Log.d(TAG, "Received item status callback: sessionId=" + sessionId
    907                             + ", sessionStatus=" + sessionStatus
    908                             + ", itemId=" + itemId
    909                             + ", itemStatus=" + itemStatus);
    910                 }
    911 
    912                 if (mStatusCallback != null) {
    913                     mStatusCallback.onItemStatusChanged(intent.getExtras(),
    914                             sessionId, sessionStatus, itemId, itemStatus);
    915                 }
    916             } else if (action.equals(ACTION_SESSION_STATUS_CHANGED)) {
    917                 if (sessionStatus == null) {
    918                     Log.w(TAG, "Discarding spurious media status callback with "
    919                             +"missing session status.");
    920                     return;
    921                 }
    922 
    923                 if (DEBUG) {
    924                     Log.d(TAG, "Received session status callback: sessionId=" + sessionId
    925                             + ", sessionStatus=" + sessionStatus);
    926                 }
    927 
    928                 if (mStatusCallback != null) {
    929                     mStatusCallback.onSessionStatusChanged(intent.getExtras(),
    930                             sessionId, sessionStatus);
    931                 }
    932             } else if (action.equals(ACTION_MESSAGE_RECEIVED)) {
    933                 if (DEBUG) {
    934                     Log.d(TAG, "Received message callback: sessionId=" + sessionId);
    935                 }
    936 
    937                 if (mOnMessageReceivedListener != null) {
    938                     mOnMessageReceivedListener.onMessageReceived(sessionId,
    939                             intent.getBundleExtra(MediaControlIntent.EXTRA_MESSAGE));
    940                 }
    941             }
    942         }
    943     }
    944 
    945     /**
    946      * A callback that will receive media status updates.
    947      */
    948     public static abstract class StatusCallback {
    949         /**
    950          * Called when the status of a media item changes.
    951          *
    952          * @param data The result data bundle.
    953          * @param sessionId The session id.
    954          * @param sessionStatus The session status, or null if unknown.
    955          * @param itemId The item id.
    956          * @param itemStatus The item status.
    957          */
    958         public void onItemStatusChanged(Bundle data,
    959                 String sessionId, MediaSessionStatus sessionStatus,
    960                 String itemId, MediaItemStatus itemStatus) {
    961         }
    962 
    963         /**
    964          * Called when the status of a media session changes.
    965          *
    966          * @param data The result data bundle.
    967          * @param sessionId The session id.
    968          * @param sessionStatus The session status, or null if unknown.
    969          */
    970         public void onSessionStatusChanged(Bundle data,
    971                 String sessionId, MediaSessionStatus sessionStatus) {
    972         }
    973 
    974         /**
    975          * Called when the session of the remote playback client changes.
    976          *
    977          * @param sessionId The new session id.
    978          */
    979         public void onSessionChanged(String sessionId) {
    980         }
    981     }
    982 
    983     /**
    984      * Base callback type for remote playback requests.
    985      */
    986     public static abstract class ActionCallback {
    987         /**
    988          * Called when a media control request fails.
    989          *
    990          * @param error A localized error message which may be shown to the user, or null
    991          * if the cause of the error is unclear.
    992          * @param code The error code, or {@link MediaControlIntent#ERROR_UNKNOWN} if unknown.
    993          * @param data The error data bundle, or null if none.
    994          */
    995         public void onError(String error, int code, Bundle data) {
    996         }
    997     }
    998 
    999     /**
   1000      * Callback for remote playback requests that operate on items.
   1001      */
   1002     public static abstract class ItemActionCallback extends ActionCallback {
   1003         /**
   1004          * Called when the request succeeds.
   1005          *
   1006          * @param data The result data bundle.
   1007          * @param sessionId The session id.
   1008          * @param sessionStatus The session status, or null if unknown.
   1009          * @param itemId The item id.
   1010          * @param itemStatus The item status.
   1011          */
   1012         public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
   1013                 String itemId, MediaItemStatus itemStatus) {
   1014         }
   1015     }
   1016 
   1017     /**
   1018      * Callback for remote playback requests that operate on sessions.
   1019      */
   1020     public static abstract class SessionActionCallback extends ActionCallback {
   1021         /**
   1022          * Called when the request succeeds.
   1023          *
   1024          * @param data The result data bundle.
   1025          * @param sessionId The session id.
   1026          * @param sessionStatus The session status, or null if unknown.
   1027          */
   1028         public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
   1029         }
   1030     }
   1031 
   1032     /**
   1033      * A callback that will receive messages from media sessions.
   1034      */
   1035     public interface OnMessageReceivedListener {
   1036         /**
   1037          * Called when a message received.
   1038          *
   1039          * @param sessionId The session id.
   1040          * @param message A bundle message denoting {@link MediaControlIntent#EXTRA_MESSAGE}.
   1041          */
   1042         void onMessageReceived(String sessionId, Bundle message);
   1043     }
   1044 }
   1045