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