| /* |
| * Copyright (C) 2013 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package android.support.v7.media; |
| |
| import android.app.PendingIntent; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.util.Log; |
| |
| /** |
| * A helper class for playing media on remote routes using the remote playback protocol |
| * defined by {@link MediaControlIntent}. |
| * <p> |
| * The client maintains session state and offers a simplified interface for issuing |
| * remote playback media control intents to a single route. |
| * </p> |
| */ |
| public class RemotePlaybackClient { |
| private static final String TAG = "RemotePlaybackClient"; |
| private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); |
| |
| private final Context mContext; |
| private final MediaRouter.RouteInfo mRoute; |
| private final StatusReceiver mStatusReceiver; |
| private final PendingIntent mItemStatusPendingIntent; |
| private final PendingIntent mSessionStatusPendingIntent; |
| |
| private boolean mRouteSupportsRemotePlayback; |
| private boolean mRouteSupportsQueuing; |
| private boolean mRouteSupportsSessionManagement; |
| |
| private String mSessionId; |
| private StatusCallback mStatusCallback; |
| |
| /** |
| * Creates a remote playback client for a route. |
| * |
| * @param route The media route. |
| */ |
| public RemotePlaybackClient(Context context, MediaRouter.RouteInfo route) { |
| if (context == null) { |
| throw new IllegalArgumentException("context must not be null"); |
| } |
| if (route == null) { |
| throw new IllegalArgumentException("route must not be null"); |
| } |
| |
| mContext = context; |
| mRoute = route; |
| |
| IntentFilter statusFilter = new IntentFilter(); |
| statusFilter.addAction(StatusReceiver.ACTION_ITEM_STATUS_CHANGED); |
| statusFilter.addAction(StatusReceiver.ACTION_SESSION_STATUS_CHANGED); |
| mStatusReceiver = new StatusReceiver(); |
| context.registerReceiver(mStatusReceiver, statusFilter); |
| |
| Intent itemStatusIntent = new Intent(StatusReceiver.ACTION_ITEM_STATUS_CHANGED); |
| itemStatusIntent.setPackage(context.getPackageName()); |
| mItemStatusPendingIntent = PendingIntent.getBroadcast( |
| context, 0, itemStatusIntent, 0); |
| |
| Intent sessionStatusIntent = new Intent(StatusReceiver.ACTION_SESSION_STATUS_CHANGED); |
| sessionStatusIntent.setPackage(context.getPackageName()); |
| mSessionStatusPendingIntent = PendingIntent.getBroadcast( |
| context, 0, sessionStatusIntent, 0); |
| |
| detectFeatures(); |
| } |
| |
| /** |
| * Releases resources owned by the client. |
| */ |
| public void release() { |
| mContext.unregisterReceiver(mStatusReceiver); |
| } |
| |
| /** |
| * Returns true if the route supports remote playback. |
| * <p> |
| * If the route does not support remote playback, then none of the functionality |
| * offered by the client will be available. |
| * </p><p> |
| * This method returns true if the route supports all of the following |
| * actions: {@link MediaControlIntent#ACTION_PLAY play}, |
| * {@link MediaControlIntent#ACTION_SEEK seek}, |
| * {@link MediaControlIntent#ACTION_GET_STATUS get status}, |
| * {@link MediaControlIntent#ACTION_PAUSE pause}, |
| * {@link MediaControlIntent#ACTION_RESUME resume}, |
| * {@link MediaControlIntent#ACTION_STOP stop}. |
| * </p> |
| * |
| * @return True if remote playback is supported. |
| */ |
| public boolean isRemotePlaybackSupported() { |
| return mRouteSupportsRemotePlayback; |
| } |
| |
| /** |
| * Returns true if the route supports queuing features. |
| * <p> |
| * If the route does not support queuing, then at most one media item can be played |
| * at a time and the {@link #enqueue} method will not be available. |
| * </p><p> |
| * This method returns true if the route supports all of the basic remote playback |
| * actions and all of the following actions: |
| * {@link MediaControlIntent#ACTION_ENQUEUE enqueue}, |
| * {@link MediaControlIntent#ACTION_REMOVE remove}. |
| * </p> |
| * |
| * @return True if queuing is supported. Implies {@link #isRemotePlaybackSupported} |
| * is also true. |
| * |
| * @see #isRemotePlaybackSupported |
| */ |
| public boolean isQueuingSupported() { |
| return mRouteSupportsQueuing; |
| } |
| |
| /** |
| * Returns true if the route supports session management features. |
| * <p> |
| * If the route does not support session management, then the session will |
| * not be created until the first media item is played. |
| * </p><p> |
| * This method returns true if the route supports all of the basic remote playback |
| * actions and all of the following actions: |
| * {@link MediaControlIntent#ACTION_START_SESSION start session}, |
| * {@link MediaControlIntent#ACTION_GET_SESSION_STATUS get session status}, |
| * {@link MediaControlIntent#ACTION_END_SESSION end session}. |
| * </p> |
| * |
| * @return True if session management is supported. |
| * Implies {@link #isRemotePlaybackSupported} is also true. |
| * |
| * @see #isRemotePlaybackSupported |
| */ |
| public boolean isSessionManagementSupported() { |
| return mRouteSupportsSessionManagement; |
| } |
| |
| /** |
| * Gets the current session id if there is one. |
| * |
| * @return The current session id, or null if none. |
| */ |
| public String getSessionId() { |
| return mSessionId; |
| } |
| |
| /** |
| * Sets the current session id. |
| * <p> |
| * It is usually not necessary to set the session id explicitly since |
| * it is created as a side-effect of other requests such as |
| * {@link #play}, {@link #enqueue}, and {@link #startSession}. |
| * </p> |
| * |
| * @param sessionId The new session id, or null if none. |
| */ |
| public void setSessionId(String sessionId) { |
| if (mSessionId != sessionId |
| && (mSessionId == null || !mSessionId.equals(sessionId))) { |
| if (DEBUG) { |
| Log.d(TAG, "Session id is now: " + sessionId); |
| } |
| mSessionId = sessionId; |
| if (mStatusCallback != null) { |
| mStatusCallback.onSessionChanged(sessionId); |
| } |
| } |
| } |
| |
| /** |
| * Returns true if the client currently has a session. |
| * <p> |
| * Equivalent to checking whether {@link #getSessionId} returns a non-null result. |
| * </p> |
| * |
| * @return True if there is a current session. |
| */ |
| public boolean hasSession() { |
| return mSessionId != null; |
| } |
| |
| /** |
| * Sets a callback that should receive status updates when the state of |
| * media sessions or media items created by this instance of the remote |
| * playback client changes. |
| * <p> |
| * The callback should be set before the session is created or any play |
| * commands are issued. |
| * </p> |
| * |
| * @param callback The callback to set. May be null to remove the previous callback. |
| */ |
| public void setStatusCallback(StatusCallback callback) { |
| mStatusCallback = callback; |
| } |
| |
| /** |
| * Sends a request to play a media item. |
| * <p> |
| * Clears the queue and starts playing the new item immediately. If the queue |
| * was previously paused, then it is resumed as a side-effect of this request. |
| * </p><p> |
| * The request is issued in the current session. If no session is available, then |
| * one is created implicitly. |
| * </p><p> |
| * Please refer to {@link MediaControlIntent#ACTION_PLAY ACTION_PLAY} for |
| * more information about the semantics of this request. |
| * </p> |
| * |
| * @param contentUri The content Uri to play. |
| * @param mimeType The mime type of the content, or null if unknown. |
| * @param positionMillis The initial content position for the item in milliseconds, |
| * or <code>0</code> to start at the beginning. |
| * @param metadata The media item metadata bundle, or null if none. |
| * @param extras A bundle of extra arguments to be added to the |
| * {@link MediaControlIntent#ACTION_PLAY} intent, or null if none. |
| * @param callback A callback to invoke when the request has been |
| * processed, or null if none. |
| * |
| * @throws UnsupportedOperationException if the route does not support remote playback. |
| * |
| * @see MediaControlIntent#ACTION_PLAY |
| * @see #isRemotePlaybackSupported |
| */ |
| public void play(Uri contentUri, String mimeType, Bundle metadata, |
| long positionMillis, Bundle extras, ItemActionCallback callback) { |
| playOrEnqueue(contentUri, mimeType, metadata, positionMillis, |
| extras, callback, MediaControlIntent.ACTION_PLAY); |
| } |
| |
| /** |
| * Sends a request to enqueue a media item. |
| * <p> |
| * Enqueues a new item to play. If the queue was previously paused, then will |
| * remain paused. |
| * </p><p> |
| * The request is issued in the current session. If no session is available, then |
| * one is created implicitly. |
| * </p><p> |
| * Please refer to {@link MediaControlIntent#ACTION_ENQUEUE ACTION_ENQUEUE} for |
| * more information about the semantics of this request. |
| * </p> |
| * |
| * @param contentUri The content Uri to enqueue. |
| * @param mimeType The mime type of the content, or null if unknown. |
| * @param positionMillis The initial content position for the item in milliseconds, |
| * or <code>0</code> to start at the beginning. |
| * @param metadata The media item metadata bundle, or null if none. |
| * @param extras A bundle of extra arguments to be added to the |
| * {@link MediaControlIntent#ACTION_ENQUEUE} intent, or null if none. |
| * @param callback A callback to invoke when the request has been |
| * processed, or null if none. |
| * |
| * @throws UnsupportedOperationException if the route does not support queuing. |
| * |
| * @see MediaControlIntent#ACTION_ENQUEUE |
| * @see #isRemotePlaybackSupported |
| * @see #isQueuingSupported |
| */ |
| public void enqueue(Uri contentUri, String mimeType, Bundle metadata, |
| long positionMillis, Bundle extras, ItemActionCallback callback) { |
| playOrEnqueue(contentUri, mimeType, metadata, positionMillis, |
| extras, callback, MediaControlIntent.ACTION_ENQUEUE); |
| } |
| |
| private void playOrEnqueue(Uri contentUri, String mimeType, Bundle metadata, |
| long positionMillis, Bundle extras, |
| final ItemActionCallback callback, String action) { |
| if (contentUri == null) { |
| throw new IllegalArgumentException("contentUri must not be null"); |
| } |
| throwIfRemotePlaybackNotSupported(); |
| if (action.equals(MediaControlIntent.ACTION_ENQUEUE)) { |
| throwIfQueuingNotSupported(); |
| } |
| |
| Intent intent = new Intent(action); |
| intent.setDataAndType(contentUri, mimeType); |
| intent.putExtra(MediaControlIntent.EXTRA_ITEM_STATUS_UPDATE_RECEIVER, |
| mItemStatusPendingIntent); |
| if (metadata != null) { |
| intent.putExtra(MediaControlIntent.EXTRA_ITEM_METADATA, metadata); |
| } |
| if (positionMillis != 0) { |
| intent.putExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, positionMillis); |
| } |
| performItemAction(intent, mSessionId, null, extras, callback); |
| } |
| |
| /** |
| * Sends a request to seek to a new position in a media item. |
| * <p> |
| * Seeks to a new position. If the queue was previously paused then it |
| * remains paused but the item's new position is still remembered. |
| * </p><p> |
| * The request is issued in the current session. |
| * </p><p> |
| * Please refer to {@link MediaControlIntent#ACTION_SEEK ACTION_SEEK} for |
| * more information about the semantics of this request. |
| * </p> |
| * |
| * @param itemId The item id. |
| * @param positionMillis The new content position for the item in milliseconds, |
| * or <code>0</code> to start at the beginning. |
| * @param extras A bundle of extra arguments to be added to the |
| * {@link MediaControlIntent#ACTION_SEEK} intent, or null if none. |
| * @param callback A callback to invoke when the request has been |
| * processed, or null if none. |
| * |
| * @throws IllegalStateException if there is no current session. |
| * |
| * @see MediaControlIntent#ACTION_SEEK |
| * @see #isRemotePlaybackSupported |
| */ |
| public void seek(String itemId, long positionMillis, Bundle extras, |
| ItemActionCallback callback) { |
| if (itemId == null) { |
| throw new IllegalArgumentException("itemId must not be null"); |
| } |
| throwIfNoCurrentSession(); |
| |
| Intent intent = new Intent(MediaControlIntent.ACTION_SEEK); |
| intent.putExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, positionMillis); |
| performItemAction(intent, mSessionId, itemId, extras, callback); |
| } |
| |
| /** |
| * Sends a request to get the status of a media item. |
| * <p> |
| * The request is issued in the current session. |
| * </p><p> |
| * Please refer to {@link MediaControlIntent#ACTION_GET_STATUS ACTION_GET_STATUS} for |
| * more information about the semantics of this request. |
| * </p> |
| * |
| * @param itemId The item id. |
| * @param extras A bundle of extra arguments to be added to the |
| * {@link MediaControlIntent#ACTION_GET_STATUS} intent, or null if none. |
| * @param callback A callback to invoke when the request has been |
| * processed, or null if none. |
| * |
| * @throws IllegalStateException if there is no current session. |
| * |
| * @see MediaControlIntent#ACTION_GET_STATUS |
| * @see #isRemotePlaybackSupported |
| */ |
| public void getStatus(String itemId, Bundle extras, ItemActionCallback callback) { |
| if (itemId == null) { |
| throw new IllegalArgumentException("itemId must not be null"); |
| } |
| throwIfNoCurrentSession(); |
| |
| Intent intent = new Intent(MediaControlIntent.ACTION_GET_STATUS); |
| performItemAction(intent, mSessionId, itemId, extras, callback); |
| } |
| |
| /** |
| * Sends a request to remove a media item from the queue. |
| * <p> |
| * The request is issued in the current session. |
| * </p><p> |
| * Please refer to {@link MediaControlIntent#ACTION_REMOVE ACTION_REMOVE} for |
| * more information about the semantics of this request. |
| * </p> |
| * |
| * @param itemId The item id. |
| * @param extras A bundle of extra arguments to be added to the |
| * {@link MediaControlIntent#ACTION_REMOVE} intent, or null if none. |
| * @param callback A callback to invoke when the request has been |
| * processed, or null if none. |
| * |
| * @throws IllegalStateException if there is no current session. |
| * @throws UnsupportedOperationException if the route does not support queuing. |
| * |
| * @see MediaControlIntent#ACTION_REMOVE |
| * @see #isRemotePlaybackSupported |
| * @see #isQueuingSupported |
| */ |
| public void remove(String itemId, Bundle extras, ItemActionCallback callback) { |
| if (itemId == null) { |
| throw new IllegalArgumentException("itemId must not be null"); |
| } |
| throwIfQueuingNotSupported(); |
| throwIfNoCurrentSession(); |
| |
| Intent intent = new Intent(MediaControlIntent.ACTION_REMOVE); |
| performItemAction(intent, mSessionId, itemId, extras, callback); |
| } |
| |
| /** |
| * Sends a request to pause media playback. |
| * <p> |
| * The request is issued in the current session. If playback is already paused |
| * then the request has no effect. |
| * </p><p> |
| * Please refer to {@link MediaControlIntent#ACTION_PAUSE ACTION_PAUSE} for |
| * more information about the semantics of this request. |
| * </p> |
| * |
| * @param extras A bundle of extra arguments to be added to the |
| * {@link MediaControlIntent#ACTION_PAUSE} intent, or null if none. |
| * @param callback A callback to invoke when the request has been |
| * processed, or null if none. |
| * |
| * @throws IllegalStateException if there is no current session. |
| * |
| * @see MediaControlIntent#ACTION_PAUSE |
| * @see #isRemotePlaybackSupported |
| */ |
| public void pause(Bundle extras, SessionActionCallback callback) { |
| throwIfNoCurrentSession(); |
| |
| Intent intent = new Intent(MediaControlIntent.ACTION_PAUSE); |
| performSessionAction(intent, mSessionId, extras, callback); |
| } |
| |
| /** |
| * Sends a request to resume (unpause) media playback. |
| * <p> |
| * The request is issued in the current session. If playback is not paused |
| * then the request has no effect. |
| * </p><p> |
| * Please refer to {@link MediaControlIntent#ACTION_RESUME ACTION_RESUME} for |
| * more information about the semantics of this request. |
| * </p> |
| * |
| * @param extras A bundle of extra arguments to be added to the |
| * {@link MediaControlIntent#ACTION_RESUME} intent, or null if none. |
| * @param callback A callback to invoke when the request has been |
| * processed, or null if none. |
| * |
| * @throws IllegalStateException if there is no current session. |
| * |
| * @see MediaControlIntent#ACTION_RESUME |
| * @see #isRemotePlaybackSupported |
| */ |
| public void resume(Bundle extras, SessionActionCallback callback) { |
| throwIfNoCurrentSession(); |
| |
| Intent intent = new Intent(MediaControlIntent.ACTION_RESUME); |
| performSessionAction(intent, mSessionId, extras, callback); |
| } |
| |
| /** |
| * Sends a request to stop media playback and clear the media playback queue. |
| * <p> |
| * The request is issued in the current session. If the queue is already |
| * empty then the request has no effect. |
| * </p><p> |
| * Please refer to {@link MediaControlIntent#ACTION_STOP ACTION_STOP} for |
| * more information about the semantics of this request. |
| * </p> |
| * |
| * @param extras A bundle of extra arguments to be added to the |
| * {@link MediaControlIntent#ACTION_STOP} intent, or null if none. |
| * @param callback A callback to invoke when the request has been |
| * processed, or null if none. |
| * |
| * @throws IllegalStateException if there is no current session. |
| * |
| * @see MediaControlIntent#ACTION_STOP |
| * @see #isRemotePlaybackSupported |
| */ |
| public void stop(Bundle extras, SessionActionCallback callback) { |
| throwIfNoCurrentSession(); |
| |
| Intent intent = new Intent(MediaControlIntent.ACTION_STOP); |
| performSessionAction(intent, mSessionId, extras, callback); |
| } |
| |
| /** |
| * Sends a request to start a new media playback session. |
| * <p> |
| * The application must wait for the callback to indicate that this request |
| * is complete before issuing other requests that affect the session. If this |
| * request is successful then the previous session will be invalidated. |
| * </p><p> |
| * Please refer to {@link MediaControlIntent#ACTION_START_SESSION ACTION_START_SESSION} |
| * for more information about the semantics of this request. |
| * </p> |
| * |
| * @param extras A bundle of extra arguments to be added to the |
| * {@link MediaControlIntent#ACTION_START_SESSION} intent, or null if none. |
| * @param callback A callback to invoke when the request has been |
| * processed, or null if none. |
| * |
| * @throws UnsupportedOperationException if the route does not support session management. |
| * |
| * @see MediaControlIntent#ACTION_START_SESSION |
| * @see #isRemotePlaybackSupported |
| * @see #isSessionManagementSupported |
| */ |
| public void startSession(Bundle extras, SessionActionCallback callback) { |
| throwIfSessionManagementNotSupported(); |
| |
| Intent intent = new Intent(MediaControlIntent.ACTION_START_SESSION); |
| intent.putExtra(MediaControlIntent.EXTRA_SESSION_STATUS_UPDATE_RECEIVER, |
| mSessionStatusPendingIntent); |
| performSessionAction(intent, null, extras, callback); |
| } |
| |
| /** |
| * Sends a request to get the status of the media playback session. |
| * <p> |
| * The request is issued in the current session. |
| * </p><p> |
| * Please refer to {@link MediaControlIntent#ACTION_GET_SESSION_STATUS |
| * ACTION_GET_SESSION_STATUS} for more information about the semantics of this request. |
| * </p> |
| * |
| * @param extras A bundle of extra arguments to be added to the |
| * {@link MediaControlIntent#ACTION_GET_SESSION_STATUS} intent, or null if none. |
| * @param callback A callback to invoke when the request has been |
| * processed, or null if none. |
| * |
| * @throws IllegalStateException if there is no current session. |
| * @throws UnsupportedOperationException if the route does not support session management. |
| * |
| * @see MediaControlIntent#ACTION_GET_SESSION_STATUS |
| * @see #isRemotePlaybackSupported |
| * @see #isSessionManagementSupported |
| */ |
| public void getSessionStatus(Bundle extras, SessionActionCallback callback) { |
| throwIfSessionManagementNotSupported(); |
| throwIfNoCurrentSession(); |
| |
| Intent intent = new Intent(MediaControlIntent.ACTION_GET_SESSION_STATUS); |
| performSessionAction(intent, mSessionId, extras, callback); |
| } |
| |
| /** |
| * Sends a request to end the media playback session. |
| * <p> |
| * The request is issued in the current session. If this request is successful, |
| * the {@link #getSessionId session id property} will be set to null after |
| * the callback is invoked. |
| * </p><p> |
| * Please refer to {@link MediaControlIntent#ACTION_END_SESSION ACTION_END_SESSION} |
| * for more information about the semantics of this request. |
| * </p> |
| * |
| * @param extras A bundle of extra arguments to be added to the |
| * {@link MediaControlIntent#ACTION_END_SESSION} intent, or null if none. |
| * @param callback A callback to invoke when the request has been |
| * processed, or null if none. |
| * |
| * @throws IllegalStateException if there is no current session. |
| * @throws UnsupportedOperationException if the route does not support session management. |
| * |
| * @see MediaControlIntent#ACTION_END_SESSION |
| * @see #isRemotePlaybackSupported |
| * @see #isSessionManagementSupported |
| */ |
| public void endSession(Bundle extras, SessionActionCallback callback) { |
| throwIfSessionManagementNotSupported(); |
| throwIfNoCurrentSession(); |
| |
| Intent intent = new Intent(MediaControlIntent.ACTION_END_SESSION); |
| performSessionAction(intent, mSessionId, extras, callback); |
| } |
| |
| private void performItemAction(final Intent intent, |
| final String sessionId, final String itemId, |
| Bundle extras, final ItemActionCallback callback) { |
| intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK); |
| if (sessionId != null) { |
| intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, sessionId); |
| } |
| if (itemId != null) { |
| intent.putExtra(MediaControlIntent.EXTRA_ITEM_ID, itemId); |
| } |
| if (extras != null) { |
| intent.putExtras(extras); |
| } |
| logRequest(intent); |
| mRoute.sendControlRequest(intent, new MediaRouter.ControlRequestCallback() { |
| @Override |
| public void onResult(Bundle data) { |
| if (data != null) { |
| String sessionIdResult = inferMissingResult(sessionId, |
| data.getString(MediaControlIntent.EXTRA_SESSION_ID)); |
| MediaSessionStatus sessionStatus = MediaSessionStatus.fromBundle( |
| data.getBundle(MediaControlIntent.EXTRA_SESSION_STATUS)); |
| String itemIdResult = inferMissingResult(itemId, |
| data.getString(MediaControlIntent.EXTRA_ITEM_ID)); |
| MediaItemStatus itemStatus = MediaItemStatus.fromBundle( |
| data.getBundle(MediaControlIntent.EXTRA_ITEM_STATUS)); |
| adoptSession(sessionIdResult); |
| if (sessionIdResult != null && itemIdResult != null && itemStatus != null) { |
| if (DEBUG) { |
| Log.d(TAG, "Received result from " + intent.getAction() |
| + ": data=" + bundleToString(data) |
| + ", sessionId=" + sessionIdResult |
| + ", sessionStatus=" + sessionStatus |
| + ", itemId=" + itemIdResult |
| + ", itemStatus=" + itemStatus); |
| } |
| callback.onResult(data, sessionIdResult, sessionStatus, |
| itemIdResult, itemStatus); |
| return; |
| } |
| } |
| handleInvalidResult(intent, callback, data); |
| } |
| |
| @Override |
| public void onError(String error, Bundle data) { |
| handleError(intent, callback, error, data); |
| } |
| }); |
| } |
| |
| private void performSessionAction(final Intent intent, final String sessionId, |
| Bundle extras, final SessionActionCallback callback) { |
| intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK); |
| if (sessionId != null) { |
| intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, sessionId); |
| } |
| if (extras != null) { |
| intent.putExtras(extras); |
| } |
| logRequest(intent); |
| mRoute.sendControlRequest(intent, new MediaRouter.ControlRequestCallback() { |
| @Override |
| public void onResult(Bundle data) { |
| if (data != null) { |
| String sessionIdResult = inferMissingResult(sessionId, |
| data.getString(MediaControlIntent.EXTRA_SESSION_ID)); |
| MediaSessionStatus sessionStatus = MediaSessionStatus.fromBundle( |
| data.getBundle(MediaControlIntent.EXTRA_SESSION_STATUS)); |
| adoptSession(sessionIdResult); |
| if (sessionIdResult != null) { |
| if (DEBUG) { |
| Log.d(TAG, "Received result from " + intent.getAction() |
| + ": data=" + bundleToString(data) |
| + ", sessionId=" + sessionIdResult |
| + ", sessionStatus=" + sessionStatus); |
| } |
| try { |
| callback.onResult(data, sessionIdResult, sessionStatus); |
| } finally { |
| if (intent.getAction().equals(MediaControlIntent.ACTION_END_SESSION) |
| && sessionIdResult.equals(mSessionId)) { |
| setSessionId(null); |
| } |
| } |
| return; |
| } |
| } |
| handleInvalidResult(intent, callback, data); |
| } |
| |
| @Override |
| public void onError(String error, Bundle data) { |
| handleError(intent, callback, error, data); |
| } |
| }); |
| } |
| |
| private void adoptSession(String sessionId) { |
| if (sessionId != null) { |
| setSessionId(sessionId); |
| } |
| } |
| |
| private void handleInvalidResult(Intent intent, ActionCallback callback, |
| Bundle data) { |
| Log.w(TAG, "Received invalid result data from " + intent.getAction() |
| + ": data=" + bundleToString(data)); |
| callback.onError(null, MediaControlIntent.ERROR_UNKNOWN, data); |
| } |
| |
| private void handleError(Intent intent, ActionCallback callback, |
| String error, Bundle data) { |
| final int code; |
| if (data != null) { |
| code = data.getInt(MediaControlIntent.EXTRA_ERROR_CODE, |
| MediaControlIntent.ERROR_UNKNOWN); |
| } else { |
| code = MediaControlIntent.ERROR_UNKNOWN; |
| } |
| if (DEBUG) { |
| Log.w(TAG, "Received error from " + intent.getAction() |
| + ": error=" + error |
| + ", code=" + code |
| + ", data=" + bundleToString(data)); |
| } |
| callback.onError(error, code, data); |
| } |
| |
| private void detectFeatures() { |
| mRouteSupportsRemotePlayback = routeSupportsAction(MediaControlIntent.ACTION_PLAY) |
| && routeSupportsAction(MediaControlIntent.ACTION_SEEK) |
| && routeSupportsAction(MediaControlIntent.ACTION_GET_STATUS) |
| && routeSupportsAction(MediaControlIntent.ACTION_PAUSE) |
| && routeSupportsAction(MediaControlIntent.ACTION_RESUME) |
| && routeSupportsAction(MediaControlIntent.ACTION_STOP); |
| mRouteSupportsQueuing = mRouteSupportsRemotePlayback |
| && routeSupportsAction(MediaControlIntent.ACTION_ENQUEUE) |
| && routeSupportsAction(MediaControlIntent.ACTION_REMOVE); |
| mRouteSupportsSessionManagement = mRouteSupportsRemotePlayback |
| && routeSupportsAction(MediaControlIntent.ACTION_START_SESSION) |
| && routeSupportsAction(MediaControlIntent.ACTION_GET_SESSION_STATUS) |
| && routeSupportsAction(MediaControlIntent.ACTION_END_SESSION); |
| } |
| |
| private boolean routeSupportsAction(String action) { |
| return mRoute.supportsControlAction(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK, action); |
| } |
| |
| private void throwIfRemotePlaybackNotSupported() { |
| if (!mRouteSupportsRemotePlayback) { |
| throw new UnsupportedOperationException("The route does not support remote playback."); |
| } |
| } |
| |
| private void throwIfQueuingNotSupported() { |
| if (!mRouteSupportsQueuing) { |
| throw new UnsupportedOperationException("The route does not support queuing."); |
| } |
| } |
| |
| private void throwIfSessionManagementNotSupported() { |
| if (!mRouteSupportsSessionManagement) { |
| throw new UnsupportedOperationException("The route does not support " |
| + "session management."); |
| } |
| } |
| |
| private void throwIfNoCurrentSession() { |
| if (mSessionId == null) { |
| throw new IllegalStateException("There is no current session."); |
| } |
| } |
| |
| private static String inferMissingResult(String request, String result) { |
| if (result == null) { |
| // Result is missing. |
| return request; |
| } |
| if (request == null || request.equals(result)) { |
| // Request didn't specify a value or result matches request. |
| return result; |
| } |
| // Result conflicts with request. |
| return null; |
| } |
| |
| private static void logRequest(Intent intent) { |
| if (DEBUG) { |
| Log.d(TAG, "Sending request: " + intent); |
| } |
| } |
| |
| private static String bundleToString(Bundle bundle) { |
| if (bundle != null) { |
| bundle.size(); // force bundle to be unparcelled |
| return bundle.toString(); |
| } |
| return "null"; |
| } |
| |
| private final class StatusReceiver extends BroadcastReceiver { |
| public static final String ACTION_ITEM_STATUS_CHANGED = |
| "android.support.v7.media.actions.ACTION_ITEM_STATUS_CHANGED"; |
| public static final String ACTION_SESSION_STATUS_CHANGED = |
| "android.support.v7.media.actions.ACTION_SESSION_STATUS_CHANGED"; |
| |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| String sessionId = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); |
| if (sessionId == null || !sessionId.equals(mSessionId)) { |
| Log.w(TAG, "Discarding spurious status callback " |
| + "with missing or invalid session id: sessionId=" + sessionId); |
| return; |
| } |
| |
| MediaSessionStatus sessionStatus = MediaSessionStatus.fromBundle( |
| intent.getBundleExtra(MediaControlIntent.EXTRA_SESSION_STATUS)); |
| String action = intent.getAction(); |
| if (action.equals(ACTION_ITEM_STATUS_CHANGED)) { |
| String itemId = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID); |
| if (itemId == null) { |
| Log.w(TAG, "Discarding spurious status callback with missing item id."); |
| return; |
| } |
| |
| MediaItemStatus itemStatus = MediaItemStatus.fromBundle( |
| intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_STATUS)); |
| if (itemStatus == null) { |
| Log.w(TAG, "Discarding spurious status callback with missing item status."); |
| return; |
| } |
| |
| if (DEBUG) { |
| Log.d(TAG, "Received item status callback: sessionId=" + sessionId |
| + ", sessionStatus=" + sessionStatus |
| + ", itemId=" + itemId |
| + ", itemStatus=" + itemStatus); |
| } |
| |
| if (mStatusCallback != null) { |
| mStatusCallback.onItemStatusChanged(intent.getExtras(), |
| sessionId, sessionStatus, itemId, itemStatus); |
| } |
| } else if (action.equals(ACTION_SESSION_STATUS_CHANGED)) { |
| if (sessionStatus == null) { |
| Log.w(TAG, "Discarding spurious media status callback with " |
| +"missing session status."); |
| return; |
| } |
| |
| if (DEBUG) { |
| Log.d(TAG, "Received session status callback: sessionId=" + sessionId |
| + ", sessionStatus=" + sessionStatus); |
| } |
| |
| if (mStatusCallback != null) { |
| mStatusCallback.onSessionStatusChanged(intent.getExtras(), |
| sessionId, sessionStatus); |
| } |
| } |
| } |
| } |
| |
| /** |
| * A callback that will receive media status updates. |
| */ |
| public static abstract class StatusCallback { |
| /** |
| * Called when the status of a media item changes. |
| * |
| * @param data The result data bundle. |
| * @param sessionId The session id. |
| * @param sessionStatus The session status, or null if unknown. |
| * @param itemId The item id. |
| * @param itemStatus The item status. |
| */ |
| public void onItemStatusChanged(Bundle data, |
| String sessionId, MediaSessionStatus sessionStatus, |
| String itemId, MediaItemStatus itemStatus) { |
| } |
| |
| /** |
| * Called when the status of a media session changes. |
| * |
| * @param data The result data bundle. |
| * @param sessionId The session id. |
| * @param sessionStatus The session status, or null if unknown. |
| */ |
| public void onSessionStatusChanged(Bundle data, |
| String sessionId, MediaSessionStatus sessionStatus) { |
| } |
| |
| /** |
| * Called when the session of the remote playback client changes. |
| * |
| * @param sessionId The new session id. |
| */ |
| public void onSessionChanged(String sessionId) { |
| } |
| } |
| |
| /** |
| * Base callback type for remote playback requests. |
| */ |
| public static abstract class ActionCallback { |
| /** |
| * Called when a media control request fails. |
| * |
| * @param error A localized error message which may be shown to the user, or null |
| * if the cause of the error is unclear. |
| * @param code The error code, or {@link MediaControlIntent#ERROR_UNKNOWN} if unknown. |
| * @param data The error data bundle, or null if none. |
| */ |
| public void onError(String error, int code, Bundle data) { |
| } |
| } |
| |
| /** |
| * Callback for remote playback requests that operate on items. |
| */ |
| public static abstract class ItemActionCallback extends ActionCallback { |
| /** |
| * Called when the request succeeds. |
| * |
| * @param data The result data bundle. |
| * @param sessionId The session id. |
| * @param sessionStatus The session status, or null if unknown. |
| * @param itemId The item id. |
| * @param itemStatus The item status. |
| */ |
| public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, |
| String itemId, MediaItemStatus itemStatus) { |
| } |
| } |
| |
| /** |
| * Callback for remote playback requests that operate on sessions. |
| */ |
| public static abstract class SessionActionCallback extends ActionCallback { |
| /** |
| * Called when the request succeeds. |
| * |
| * @param data The result data bundle. |
| * @param sessionId The session id. |
| * @param sessionStatus The session status, or null if unknown. |
| */ |
| public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { |
| } |
| } |
| } |