/* * 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; import java.util.Iterator; /** * A helper class for playing media on remote routes using the remote playback protocol * defined by {@link MediaControlIntent}. *
* The client maintains session state and offers a simplified interface for issuing * remote playback media control intents to a single route. *
*/ public class RemotePlaybackClient { static final String TAG = "RemotePlaybackClient"; static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private final Context mContext; private final MediaRouter.RouteInfo mRoute; private final ActionReceiver mActionReceiver; private final PendingIntent mItemStatusPendingIntent; private final PendingIntent mSessionStatusPendingIntent; private final PendingIntent mMessagePendingIntent; private boolean mRouteSupportsRemotePlayback; private boolean mRouteSupportsQueuing; private boolean mRouteSupportsSessionManagement; private boolean mRouteSupportsMessaging; String mSessionId; StatusCallback mStatusCallback; OnMessageReceivedListener mOnMessageReceivedListener; /** * 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 actionFilter = new IntentFilter(); actionFilter.addAction(ActionReceiver.ACTION_ITEM_STATUS_CHANGED); actionFilter.addAction(ActionReceiver.ACTION_SESSION_STATUS_CHANGED); actionFilter.addAction(ActionReceiver.ACTION_MESSAGE_RECEIVED); mActionReceiver = new ActionReceiver(); context.registerReceiver(mActionReceiver, actionFilter); Intent itemStatusIntent = new Intent(ActionReceiver.ACTION_ITEM_STATUS_CHANGED); itemStatusIntent.setPackage(context.getPackageName()); mItemStatusPendingIntent = PendingIntent.getBroadcast( context, 0, itemStatusIntent, 0); Intent sessionStatusIntent = new Intent(ActionReceiver.ACTION_SESSION_STATUS_CHANGED); sessionStatusIntent.setPackage(context.getPackageName()); mSessionStatusPendingIntent = PendingIntent.getBroadcast( context, 0, sessionStatusIntent, 0); Intent messageIntent = new Intent(ActionReceiver.ACTION_MESSAGE_RECEIVED); messageIntent.setPackage(context.getPackageName()); mMessagePendingIntent = PendingIntent.getBroadcast( context, 0, messageIntent, 0); detectFeatures(); } /** * Releases resources owned by the client. */ public void release() { mContext.unregisterReceiver(mActionReceiver); } /** * Returns true if the route supports remote playback. ** If the route does not support remote playback, then none of the functionality * offered by the client will be available. *
* 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}. *
* * @return True if remote playback is supported. */ public boolean isRemotePlaybackSupported() { return mRouteSupportsRemotePlayback; } /** * Returns true if the route supports queuing features. ** 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. *
* 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}. *
* * @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. ** If the route does not support session management, then the session will * not be created until the first media item is played. *
* 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}. *
* * @return True if session management is supported. * Implies {@link #isRemotePlaybackSupported} is also true. * * @see #isRemotePlaybackSupported */ public boolean isSessionManagementSupported() { return mRouteSupportsSessionManagement; } /** * Returns true if the route supports messages. ** 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_SEND_MESSAGE send message}, * {@link MediaControlIntent#ACTION_END_SESSION end session}. *
* * @return True if session management is supported. * Implies {@link #isRemotePlaybackSupported} is also true. * * @see #isRemotePlaybackSupported */ public boolean isMessagingSupported() { return mRouteSupportsMessaging; } /** * 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. ** 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}. *
* * @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. ** Equivalent to checking whether {@link #getSessionId} returns a non-null result. *
* * @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. ** The callback should be set before the session is created or any play * commands are issued. *
* * @param callback The callback to set. May be null to remove the previous callback. */ public void setStatusCallback(StatusCallback callback) { mStatusCallback = callback; } /** * Sets a callback that should receive messages when a message is sent from * media sessions created by this instance of the remote playback client changes. ** The callback should be set before the session is created. *
* * @param listener The callback to set. May be null to remove the previous callback. */ public void setOnMessageReceivedListener(OnMessageReceivedListener listener) { mOnMessageReceivedListener = listener; } /** * Sends a request to play a media item. ** 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. *
* The request is issued in the current session. If no session is available, then * one is created implicitly. *
* Please refer to {@link MediaControlIntent#ACTION_PLAY ACTION_PLAY} for * more information about the semantics of this request. *
* * @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, * or0
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.
* * Enqueues a new item to play. If the queue was previously paused, then will * remain paused. *
* The request is issued in the current session. If no session is available, then * one is created implicitly. *
* Please refer to {@link MediaControlIntent#ACTION_ENQUEUE ACTION_ENQUEUE} for * more information about the semantics of this request. *
* * @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, * or0
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.
* * Seeks to a new position. If the queue was previously paused then it * remains paused but the item's new position is still remembered. *
* The request is issued in the current session. *
* Please refer to {@link MediaControlIntent#ACTION_SEEK ACTION_SEEK} for * more information about the semantics of this request. *
* * @param itemId The item id. * @param positionMillis The new content position for the item in milliseconds, * or0
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.
* * The request is issued in the current session. *
* Please refer to {@link MediaControlIntent#ACTION_GET_STATUS ACTION_GET_STATUS} for * more information about the semantics of this request. *
* * @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. ** The request is issued in the current session. *
* Please refer to {@link MediaControlIntent#ACTION_REMOVE ACTION_REMOVE} for * more information about the semantics of this request. *
* * @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. ** The request is issued in the current session. If playback is already paused * then the request has no effect. *
* Please refer to {@link MediaControlIntent#ACTION_PAUSE ACTION_PAUSE} for * more information about the semantics of this request. *
* * @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. ** The request is issued in the current session. If playback is not paused * then the request has no effect. *
* Please refer to {@link MediaControlIntent#ACTION_RESUME ACTION_RESUME} for * more information about the semantics of this request. *
* * @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. ** The request is issued in the current session. If the queue is already * empty then the request has no effect. *
* Please refer to {@link MediaControlIntent#ACTION_STOP ACTION_STOP} for * more information about the semantics of this request. *
* * @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. ** 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. *
* Please refer to {@link MediaControlIntent#ACTION_START_SESSION ACTION_START_SESSION} * for more information about the semantics of this request. *
* * @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); if (mRouteSupportsMessaging) { intent.putExtra(MediaControlIntent.EXTRA_MESSAGE_RECEIVER, mMessagePendingIntent); } performSessionAction(intent, null, extras, callback); } /** * Sends a message. ** The request is issued in the current session. *
* Please refer to {@link MediaControlIntent#ACTION_SEND_MESSAGE} for * more information about the semantics of this request. *
* * @param message A bundle message denoting {@link MediaControlIntent#EXTRA_MESSAGE}. * @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 messages. * * @see MediaControlIntent#ACTION_SEND_MESSAGE * @see #isMessagingSupported */ public void sendMessage(Bundle message, SessionActionCallback callback) { throwIfNoCurrentSession(); throwIfMessageNotSupported(); Intent intent = new Intent(MediaControlIntent.ACTION_SEND_MESSAGE); performSessionAction(intent, mSessionId, message, callback); } /** * Sends a request to get the status of the media playback session. ** The request is issued in the current session. *
* Please refer to {@link MediaControlIntent#ACTION_GET_SESSION_STATUS * ACTION_GET_SESSION_STATUS} for more information about the semantics of this request. *
* * @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. ** 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. *
* Please refer to {@link MediaControlIntent#ACTION_END_SESSION ACTION_END_SESSION} * for more information about the semantics of this request. *
* * @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); } }); } void adoptSession(String sessionId) { if (sessionId != null) { setSessionId(sessionId); } } 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); } 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); mRouteSupportsMessaging = doesRouteSupportMessaging(); } private boolean routeSupportsAction(String action) { return mRoute.supportsControlAction(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK, action); } private boolean doesRouteSupportMessaging() { for (IntentFilter filter : mRoute.getControlFilters()) { if (filter.hasAction(MediaControlIntent.ACTION_SEND_MESSAGE)) { return true; } } return false; } 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 throwIfMessageNotSupported() { if (!mRouteSupportsMessaging) { throw new UnsupportedOperationException("The route does not support message."); } } private void throwIfNoCurrentSession() { if (mSessionId == null) { throw new IllegalStateException("There is no current session."); } } 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); } } static String bundleToString(Bundle bundle) { if (bundle != null) { bundle.size(); // force bundle to be unparcelled return bundle.toString(); } return "null"; } private final class ActionReceiver 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"; public static final String ACTION_MESSAGE_RECEIVED = "android.support.v7.media.actions.ACTION_MESSAGE_RECEIVED"; ActionReceiver() { } @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); } } else if (action.equals(ACTION_MESSAGE_RECEIVED)) { if (DEBUG) { Log.d(TAG, "Received message callback: sessionId=" + sessionId); } if (mOnMessageReceivedListener != null) { mOnMessageReceivedListener.onMessageReceived(sessionId, intent.getBundleExtra(MediaControlIntent.EXTRA_MESSAGE)); } } } } /** * 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) { } } /** * A callback that will receive messages from media sessions. */ public interface OnMessageReceivedListener { /** * Called when a message received. * * @param sessionId The session id. * @param message A bundle message denoting {@link MediaControlIntent#EXTRA_MESSAGE}. */ void onMessageReceived(String sessionId, Bundle message); } }