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