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