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