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