1/* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16package android.support.v7.media; 17 18import android.app.PendingIntent; 19import android.content.BroadcastReceiver; 20import android.content.Context; 21import android.content.Intent; 22import android.content.IntentFilter; 23import android.net.Uri; 24import android.os.Bundle; 25import android.util.Log; 26 27import 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 */ 37public 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 */ 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 (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 */ 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 */ 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 */ 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 */ 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 */ 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 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 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 751 private void adoptSession(String sessionId) { 752 if (sessionId != null) { 753 setSessionId(sessionId); 754 } 755 } 756 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 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 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 799 private boolean routeSupportsAction(String action) { 800 return mRoute.supportsControlAction(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK, action); 801 } 802 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 812 private void throwIfRemotePlaybackNotSupported() { 813 if (!mRouteSupportsRemotePlayback) { 814 throw new UnsupportedOperationException("The route does not support remote playback."); 815 } 816 } 817 818 private void throwIfQueuingNotSupported() { 819 if (!mRouteSupportsQueuing) { 820 throw new UnsupportedOperationException("The route does not support queuing."); 821 } 822 } 823 824 private void throwIfSessionManagementNotSupported() { 825 if (!mRouteSupportsSessionManagement) { 826 throw new UnsupportedOperationException("The route does not support " 827 + "session management."); 828 } 829 } 830 831 private void throwIfMessageNotSupported() { 832 if (!mRouteSupportsMessaging) { 833 throw new UnsupportedOperationException("The route does not support message."); 834 } 835 } 836 837 private void throwIfNoCurrentSession() { 838 if (mSessionId == null) { 839 throw new IllegalStateException("There is no current session."); 840 } 841 } 842 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 856 private static void logRequest(Intent intent) { 857 if (DEBUG) { 858 Log.d(TAG, "Sending request: " + intent); 859 } 860 } 861 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 1041 void onMessageReceived(String sessionId, Bundle message); 1042 } 1043} 1044