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