MediaSession.java revision bf58d9b727f1007c7c620f622ac1d8003b1b211b
1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.media.session; 18 19import android.annotation.NonNull; 20import android.annotation.Nullable; 21import android.app.PendingIntent; 22import android.content.ComponentName; 23import android.content.Intent; 24import android.media.AudioManager; 25import android.media.MediaMetadata; 26import android.media.Rating; 27import android.media.VolumeProvider; 28import android.media.session.ISessionController; 29import android.media.session.ISession; 30import android.media.session.ISessionCallback; 31import android.os.Bundle; 32import android.os.Handler; 33import android.os.Looper; 34import android.os.Message; 35import android.os.RemoteException; 36import android.os.ResultReceiver; 37import android.text.TextUtils; 38import android.util.ArrayMap; 39import android.util.Log; 40 41import java.lang.ref.WeakReference; 42import java.util.ArrayList; 43import java.util.List; 44 45/** 46 * Allows interaction with media controllers, volume keys, media buttons, and 47 * transport controls. 48 * <p> 49 * A MediaSession should be created when an app wants to publish media playback 50 * information or handle media keys. In general an app only needs one session 51 * for all playback, though multiple sessions can be created to provide finer 52 * grain controls of media. 53 * <p> 54 * Once a session is created the owner of the session may pass its 55 * {@link #getSessionToken() session token} to other processes to allow them to 56 * create a {@link MediaController} to interact with the session. 57 * <p> 58 * To receive commands, media keys, and other events a {@link Callback} must be 59 * set with {@link #addCallback(Callback)}. To receive transport control 60 * commands a {@link TransportControlsCallback} must be set with 61 * {@link #addTransportControlsCallback}. 62 * <p> 63 * When an app is finished performing playback it must call {@link #release()} 64 * to clean up the session and notify any controllers. 65 * <p> 66 * MediaSession objects are thread safe. 67 */ 68public final class MediaSession { 69 private static final String TAG = "MediaSession"; 70 71 /** 72 * Set this flag on the session to indicate that it can handle media button 73 * events. 74 */ 75 public static final int FLAG_HANDLES_MEDIA_BUTTONS = 1 << 0; 76 77 /** 78 * Set this flag on the session to indicate that it handles transport 79 * control commands through a {@link TransportControlsCallback}. 80 * The callback can be retrieved by calling {@link #addTransportControlsCallback}. 81 */ 82 public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 1 << 1; 83 84 /** 85 * System only flag for a session that needs to have priority over all other 86 * sessions. This flag ensures this session will receive media button events 87 * regardless of the current ordering in the system. 88 * 89 * @hide 90 */ 91 public static final int FLAG_EXCLUSIVE_GLOBAL_PRIORITY = 1 << 16; 92 93 /** 94 * Indicates the session was disconnected because the user that the session 95 * belonged to is stopping. 96 * 97 * @hide 98 */ 99 public static final int DISCONNECT_REASON_USER_STOPPING = 1; 100 101 /** 102 * Indicates the session was disconnected because the provider disconnected 103 * the route. 104 * @hide 105 */ 106 public static final int DISCONNECT_REASON_PROVIDER_DISCONNECTED = 2; 107 108 /** 109 * Indicates the session was disconnected because the route has changed. 110 * @hide 111 */ 112 public static final int DISCONNECT_REASON_ROUTE_CHANGED = 3; 113 114 /** 115 * Indicates the session was disconnected because the session owner 116 * requested it disconnect. 117 * @hide 118 */ 119 public static final int DISCONNECT_REASON_SESSION_DISCONNECTED = 4; 120 121 /** 122 * Indicates the session was disconnected because it was destroyed. 123 * @hide 124 */ 125 public static final int DISCONNECT_REASON_SESSION_DESTROYED = 5; 126 127 /** 128 * The session uses local playback. 129 */ 130 public static final int VOLUME_TYPE_LOCAL = 1; 131 132 /** 133 * The session uses remote playback. 134 */ 135 public static final int VOLUME_TYPE_REMOTE = 2; 136 137 private final Object mLock = new Object(); 138 139 private final MediaSessionToken mSessionToken; 140 private final ISession mBinder; 141 private final CallbackStub mCbStub; 142 143 private final ArrayList<CallbackMessageHandler> mCallbacks 144 = new ArrayList<CallbackMessageHandler>(); 145 private final ArrayList<TransportMessageHandler> mTransportCallbacks 146 = new ArrayList<TransportMessageHandler>(); 147 // TODO route interfaces 148 private final ArrayMap<String, RouteInterface.EventListener> mInterfaceListeners 149 = new ArrayMap<String, RouteInterface.EventListener>(); 150 151 private Route mRoute; 152 private VolumeProvider mVolumeProvider; 153 154 private boolean mActive = false; 155 156 /** 157 * @hide 158 */ 159 public MediaSession(ISession binder, CallbackStub cbStub) { 160 mBinder = binder; 161 mCbStub = cbStub; 162 ISessionController controllerBinder = null; 163 try { 164 controllerBinder = mBinder.getController(); 165 } catch (RemoteException e) { 166 throw new RuntimeException("Dead object in MediaSessionController constructor: ", e); 167 } 168 mSessionToken = new MediaSessionToken(controllerBinder); 169 } 170 171 /** 172 * Add a callback to receive updates on for the MediaSession. This includes 173 * media button and volume events. The caller's thread will be used to post 174 * events. 175 * 176 * @param callback The callback object 177 */ 178 public void addCallback(@NonNull Callback callback) { 179 addCallback(callback, null); 180 } 181 182 /** 183 * Add a callback to receive updates for the MediaSession. This includes 184 * media button and volume events. 185 * 186 * @param callback The callback to receive updates on. 187 * @param handler The handler that events should be posted on. 188 */ 189 public void addCallback(@NonNull Callback callback, @Nullable Handler handler) { 190 if (callback == null) { 191 throw new IllegalArgumentException("Callback cannot be null"); 192 } 193 synchronized (mLock) { 194 if (getHandlerForCallbackLocked(callback) != null) { 195 Log.w(TAG, "Callback is already added, ignoring"); 196 return; 197 } 198 if (handler == null) { 199 handler = new Handler(); 200 } 201 CallbackMessageHandler msgHandler = new CallbackMessageHandler(handler.getLooper(), 202 callback); 203 mCallbacks.add(msgHandler); 204 } 205 } 206 207 /** 208 * Remove a callback. It will no longer receive updates. 209 * 210 * @param callback The callback to remove. 211 */ 212 public void removeCallback(@NonNull Callback callback) { 213 synchronized (mLock) { 214 removeCallbackLocked(callback); 215 } 216 } 217 218 /** 219 * Set an intent for launching UI for this Session. This can be used as a 220 * quick link to an ongoing media screen. 221 * 222 * @param pi The intent to launch to show UI for this Session. 223 */ 224 public void setLaunchPendingIntent(@Nullable PendingIntent pi) { 225 // TODO 226 } 227 228 /** 229 * Set a media button event receiver component to use to restart playback 230 * after an app has been stopped. 231 * 232 * @param mbr The receiver component to send the media button event to. 233 * @hide 234 */ 235 public void setMediaButtonReceiver(@Nullable ComponentName mbr) { 236 try { 237 mBinder.setMediaButtonReceiver(mbr); 238 } catch (RemoteException e) { 239 Log.wtf(TAG, "Failure in setMediaButtonReceiver.", e); 240 } 241 } 242 243 /** 244 * Set any flags for the session. 245 * 246 * @param flags The flags to set for this session. 247 */ 248 public void setFlags(int flags) { 249 try { 250 mBinder.setFlags(flags); 251 } catch (RemoteException e) { 252 Log.wtf(TAG, "Failure in setFlags.", e); 253 } 254 } 255 256 /** 257 * Set the stream this session is playing on. This will affect the system's 258 * volume handling for this session. If {@link #setPlaybackToRemote} was 259 * previously called it will stop receiving volume commands and the system 260 * will begin sending volume changes to the appropriate stream. 261 * <p> 262 * By default sessions are on {@link AudioManager#STREAM_MUSIC}. 263 * 264 * @param stream The {@link AudioManager} stream this session is playing on. 265 */ 266 public void setPlaybackToLocal(int stream) { 267 try { 268 mBinder.configureVolumeHandling(VOLUME_TYPE_LOCAL, stream, 0); 269 } catch (RemoteException e) { 270 Log.wtf(TAG, "Failure in setPlaybackToLocal.", e); 271 } 272 } 273 274 /** 275 * Configure this session to use remote volume handling. This must be called 276 * to receive volume button events, otherwise the system will adjust the 277 * current stream volume for this session. If {@link #setPlaybackToLocal} 278 * was previously called that stream will stop receiving volume changes for 279 * this session. 280 * 281 * @param volumeProvider The provider that will handle volume changes. May 282 * not be null. 283 */ 284 public void setPlaybackToRemote(@NonNull VolumeProvider volumeProvider) { 285 if (volumeProvider == null) { 286 throw new IllegalArgumentException("volumeProvider may not be null!"); 287 } 288 mVolumeProvider = volumeProvider; 289 volumeProvider.setCallback(new VolumeProvider.Callback() { 290 @Override 291 public void onVolumeChanged(VolumeProvider volumeProvider) { 292 notifyRemoteVolumeChanged(volumeProvider); 293 } 294 }); 295 296 try { 297 mBinder.configureVolumeHandling(VOLUME_TYPE_REMOTE, volumeProvider.getVolumeControl(), 298 volumeProvider.getMaxVolume()); 299 } catch (RemoteException e) { 300 Log.wtf(TAG, "Failure in setPlaybackToRemote.", e); 301 } 302 } 303 304 /** 305 * Set if this session is currently active and ready to receive commands. If 306 * set to false your session's controller may not be discoverable. You must 307 * set the session to active before it can start receiving media button 308 * events or transport commands. 309 * 310 * @param active Whether this session is active or not. 311 */ 312 public void setActive(boolean active) { 313 if (mActive == active) { 314 return; 315 } 316 try { 317 mBinder.setActive(active); 318 mActive = active; 319 } catch (RemoteException e) { 320 Log.wtf(TAG, "Failure in setActive.", e); 321 } 322 } 323 324 /** 325 * Get the current active state of this session. 326 * 327 * @return True if the session is active, false otherwise. 328 */ 329 public boolean isActive() { 330 return mActive; 331 } 332 333 /** 334 * Send a proprietary event to all MediaControllers listening to this 335 * Session. It's up to the Controller/Session owner to determine the meaning 336 * of any events. 337 * 338 * @param event The name of the event to send 339 * @param extras Any extras included with the event 340 */ 341 public void sendSessionEvent(@NonNull String event, @Nullable Bundle extras) { 342 if (TextUtils.isEmpty(event)) { 343 throw new IllegalArgumentException("event cannot be null or empty"); 344 } 345 try { 346 mBinder.sendEvent(event, extras); 347 } catch (RemoteException e) { 348 Log.wtf(TAG, "Error sending event", e); 349 } 350 } 351 352 /** 353 * This must be called when an app has finished performing playback. If 354 * playback is expected to start again shortly the session can be left open, 355 * but it must be released if your activity or service is being destroyed. 356 */ 357 public void release() { 358 try { 359 mBinder.destroy(); 360 } catch (RemoteException e) { 361 Log.wtf(TAG, "Error releasing session: ", e); 362 } 363 } 364 365 /** 366 * Retrieve a token object that can be used by apps to create a 367 * {@link MediaController} for interacting with this session. The owner of 368 * the session is responsible for deciding how to distribute these tokens. 369 * 370 * @return A token that can be used to create a MediaController for this 371 * session 372 */ 373 public @NonNull MediaSessionToken getSessionToken() { 374 return mSessionToken; 375 } 376 377 /** 378 * Connect to the current route using the specified request. 379 * <p> 380 * Connection updates will be sent to the callback's 381 * {@link Callback#onRouteConnected(Route)} and 382 * {@link Callback#onRouteDisconnected(Route, int)} methods. If the 383 * connection fails {@link Callback#onRouteDisconnected(Route, int)} will be 384 * called. 385 * <p> 386 * If you already have a connection to this route it will be disconnected 387 * before the new connection is established. TODO add an easy way to compare 388 * MediaRouteOptions. 389 * 390 * @param route The route the app is trying to connect to. 391 * @param request The connection request to use. 392 * @hide 393 */ 394 public void connect(RouteInfo route, RouteOptions request) { 395 if (route == null) { 396 throw new IllegalArgumentException("Must specify the route"); 397 } 398 if (request == null) { 399 throw new IllegalArgumentException("Must specify the connection request"); 400 } 401 try { 402 mBinder.connectToRoute(route, request); 403 } catch (RemoteException e) { 404 Log.wtf(TAG, "Error starting connection to route", e); 405 } 406 } 407 408 /** 409 * Disconnect from the current route. After calling you will be switched 410 * back to the default route. 411 * 412 * @hide 413 */ 414 public void disconnect() { 415 if (mRoute != null) { 416 try { 417 mBinder.disconnectFromRoute(mRoute.getRouteInfo()); 418 } catch (RemoteException e) { 419 Log.wtf(TAG, "Error disconnecting from route"); 420 } 421 } 422 } 423 424 /** 425 * Set the list of route options your app is interested in connecting to. It 426 * will be used for picking valid routes. 427 * 428 * @param options The set of route options your app may use to connect. 429 * @hide 430 */ 431 public void setRouteOptions(List<RouteOptions> options) { 432 try { 433 mBinder.setRouteOptions(options); 434 } catch (RemoteException e) { 435 Log.wtf(TAG, "Error setting route options.", e); 436 } 437 } 438 439 /** 440 * @hide 441 * TODO allow multiple listeners for the same interface, allow removal 442 */ 443 public void addInterfaceListener(String iface, 444 RouteInterface.EventListener listener) { 445 mInterfaceListeners.put(iface, listener); 446 } 447 448 /** 449 * @hide 450 */ 451 public boolean sendRouteCommand(RouteCommand command, ResultReceiver cb) { 452 try { 453 mBinder.sendRouteCommand(command, cb); 454 } catch (RemoteException e) { 455 Log.wtf(TAG, "Error sending command to route.", e); 456 return false; 457 } 458 return true; 459 } 460 461 /** 462 * Add a callback to receive transport controls on, such as play, rewind, or 463 * fast forward. 464 * 465 * @param callback The callback object 466 */ 467 public void addTransportControlsCallback(@NonNull TransportControlsCallback callback) { 468 addTransportControlsCallback(callback, null); 469 } 470 471 /** 472 * Add a callback to receive transport controls on, such as play, rewind, or 473 * fast forward. The updates will be posted to the specified handler. If no 474 * handler is provided they will be posted to the caller's thread. 475 * 476 * @param callback The callback to receive updates on 477 * @param handler The handler to post the updates on 478 */ 479 public void addTransportControlsCallback(@NonNull TransportControlsCallback callback, 480 @Nullable Handler handler) { 481 if (callback == null) { 482 throw new IllegalArgumentException("Callback cannot be null"); 483 } 484 synchronized (mLock) { 485 if (getTransportControlsHandlerForCallbackLocked(callback) != null) { 486 Log.w(TAG, "Callback is already added, ignoring"); 487 return; 488 } 489 if (handler == null) { 490 handler = new Handler(); 491 } 492 TransportMessageHandler msgHandler = new TransportMessageHandler(handler.getLooper(), 493 callback); 494 mTransportCallbacks.add(msgHandler); 495 } 496 } 497 498 /** 499 * Stop receiving transport controls on the specified callback. If an update 500 * has already been posted you may still receive it after this call returns. 501 * 502 * @param callback The callback to stop receiving updates on 503 */ 504 public void removeTransportControlsCallback(@NonNull TransportControlsCallback callback) { 505 if (callback == null) { 506 throw new IllegalArgumentException("Callback cannot be null"); 507 } 508 synchronized (mLock) { 509 removeTransportControlsCallbackLocked(callback); 510 } 511 } 512 513 /** 514 * Update the current playback state. 515 * 516 * @param state The current state of playback 517 */ 518 public void setPlaybackState(@Nullable PlaybackState state) { 519 try { 520 mBinder.setPlaybackState(state); 521 } catch (RemoteException e) { 522 Log.wtf(TAG, "Dead object in setPlaybackState.", e); 523 } 524 } 525 526 /** 527 * Update the current metadata. New metadata can be created using 528 * {@link android.media.MediaMetadata.Builder}. 529 * 530 * @param metadata The new metadata 531 */ 532 public void setMetadata(@Nullable MediaMetadata metadata) { 533 try { 534 mBinder.setMetadata(metadata); 535 } catch (RemoteException e) { 536 Log.wtf(TAG, "Dead object in setPlaybackState.", e); 537 } 538 } 539 540 /** 541 * Notify the system that the remote volume changed. 542 * 543 * @param provider The provider that is handling volume changes. 544 * @hide 545 */ 546 public void notifyRemoteVolumeChanged(VolumeProvider provider) { 547 if (provider == null || provider != mVolumeProvider) { 548 Log.w(TAG, "Received update from stale volume provider"); 549 return; 550 } 551 try { 552 mBinder.setCurrentVolume(provider.onGetCurrentVolume()); 553 } catch (RemoteException e) { 554 Log.e(TAG, "Error in notifyVolumeChanged", e); 555 } 556 } 557 558 private void dispatchPlay() { 559 postToTransportCallbacks(TransportMessageHandler.MSG_PLAY); 560 } 561 562 private void dispatchPause() { 563 postToTransportCallbacks(TransportMessageHandler.MSG_PAUSE); 564 } 565 566 private void dispatchStop() { 567 postToTransportCallbacks(TransportMessageHandler.MSG_STOP); 568 } 569 570 private void dispatchNext() { 571 postToTransportCallbacks(TransportMessageHandler.MSG_NEXT); 572 } 573 574 private void dispatchPrevious() { 575 postToTransportCallbacks(TransportMessageHandler.MSG_PREVIOUS); 576 } 577 578 private void dispatchFastForward() { 579 postToTransportCallbacks(TransportMessageHandler.MSG_FAST_FORWARD); 580 } 581 582 private void dispatchRewind() { 583 postToTransportCallbacks(TransportMessageHandler.MSG_REWIND); 584 } 585 586 private void dispatchSeekTo(long pos) { 587 postToTransportCallbacks(TransportMessageHandler.MSG_SEEK_TO, pos); 588 } 589 590 private void dispatchRate(Rating rating) { 591 postToTransportCallbacks(TransportMessageHandler.MSG_RATE, rating); 592 } 593 594 private TransportMessageHandler getTransportControlsHandlerForCallbackLocked( 595 TransportControlsCallback callback) { 596 for (int i = mTransportCallbacks.size() - 1; i >= 0; i--) { 597 TransportMessageHandler handler = mTransportCallbacks.get(i); 598 if (callback == handler.mCallback) { 599 return handler; 600 } 601 } 602 return null; 603 } 604 605 private boolean removeTransportControlsCallbackLocked(TransportControlsCallback callback) { 606 for (int i = mTransportCallbacks.size() - 1; i >= 0; i--) { 607 if (callback == mTransportCallbacks.get(i).mCallback) { 608 mTransportCallbacks.remove(i); 609 return true; 610 } 611 } 612 return false; 613 } 614 615 private void postToTransportCallbacks(int what, Object obj) { 616 synchronized (mLock) { 617 for (int i = mTransportCallbacks.size() - 1; i >= 0; i--) { 618 mTransportCallbacks.get(i).post(what, obj); 619 } 620 } 621 } 622 623 private void postToTransportCallbacks(int what) { 624 postToTransportCallbacks(what, null); 625 } 626 627 private CallbackMessageHandler getHandlerForCallbackLocked(Callback cb) { 628 if (cb == null) { 629 throw new IllegalArgumentException("Callback cannot be null"); 630 } 631 for (int i = mCallbacks.size() - 1; i >= 0; i--) { 632 CallbackMessageHandler handler = mCallbacks.get(i); 633 if (cb == handler.mCallback) { 634 return handler; 635 } 636 } 637 return null; 638 } 639 640 private boolean removeCallbackLocked(Callback cb) { 641 if (cb == null) { 642 throw new IllegalArgumentException("Callback cannot be null"); 643 } 644 for (int i = mCallbacks.size() - 1; i >= 0; i--) { 645 CallbackMessageHandler handler = mCallbacks.get(i); 646 if (cb == handler.mCallback) { 647 mCallbacks.remove(i); 648 return true; 649 } 650 } 651 return false; 652 } 653 654 private void postCommand(String command, Bundle extras, ResultReceiver resultCb) { 655 Command cmd = new Command(command, extras, resultCb); 656 synchronized (mLock) { 657 for (int i = mCallbacks.size() - 1; i >= 0; i--) { 658 mCallbacks.get(i).post(CallbackMessageHandler.MSG_COMMAND, cmd); 659 } 660 } 661 } 662 663 private void postMediaButton(Intent mediaButtonIntent) { 664 synchronized (mLock) { 665 for (int i = mCallbacks.size() - 1; i >= 0; i--) { 666 mCallbacks.get(i).post(CallbackMessageHandler.MSG_MEDIA_BUTTON, mediaButtonIntent); 667 } 668 } 669 } 670 671 private void postRequestRouteChange(RouteInfo route) { 672 synchronized (mLock) { 673 for (int i = mCallbacks.size() - 1; i >= 0; i--) { 674 mCallbacks.get(i).post(CallbackMessageHandler.MSG_ROUTE_CHANGE, route); 675 } 676 } 677 } 678 679 private void postRouteConnected(RouteInfo route, RouteOptions options) { 680 synchronized (mLock) { 681 mRoute = new Route(route, options, this); 682 for (int i = mCallbacks.size() - 1; i >= 0; i--) { 683 mCallbacks.get(i).post(CallbackMessageHandler.MSG_ROUTE_CONNECTED, mRoute); 684 } 685 } 686 } 687 688 private void postRouteDisconnected(RouteInfo route, int reason) { 689 synchronized (mLock) { 690 if (mRoute != null && TextUtils.equals(mRoute.getRouteInfo().getId(), route.getId())) { 691 for (int i = mCallbacks.size() - 1; i >= 0; i--) { 692 mCallbacks.get(i).post(CallbackMessageHandler.MSG_ROUTE_DISCONNECTED, mRoute, 693 reason); 694 } 695 } 696 } 697 } 698 699 /** 700 * Receives generic commands or updates from controllers and the system. 701 * Callbacks may be registered using {@link #addCallback}. 702 */ 703 public abstract static class Callback { 704 705 public Callback() { 706 } 707 708 /** 709 * Called when a media button is pressed and this session has the 710 * highest priority or a controller sends a media button event to the 711 * session. TODO determine if using Intents identical to the ones 712 * RemoteControlClient receives is useful 713 * <p> 714 * The intent will be of type {@link Intent#ACTION_MEDIA_BUTTON} with a 715 * KeyEvent in {@link Intent#EXTRA_KEY_EVENT} 716 * 717 * @param mediaButtonIntent an intent containing the KeyEvent as an 718 * extra 719 */ 720 public void onMediaButtonEvent(@NonNull Intent mediaButtonIntent) { 721 } 722 723 /** 724 * Called when a controller has sent a custom command to this session. 725 * The owner of the session may handle custom commands but is not 726 * required to. 727 * 728 * @param command The command name. 729 * @param extras Optional parameters for the command, may be null. 730 * @param cb A result receiver to which a result may be sent by the command, may be null. 731 */ 732 public void onControlCommand(@NonNull String command, @Nullable Bundle extras, 733 @Nullable ResultReceiver cb) { 734 } 735 736 /** 737 * Called when the user has selected a different route to connect to. 738 * The app is responsible for connecting to the new route and migrating 739 * ongoing playback if necessary. 740 * 741 * @param route 742 * @hide 743 */ 744 public void onRequestRouteChange(RouteInfo route) { 745 } 746 747 /** 748 * Called when a route has successfully connected. Calls to the route 749 * are now valid. 750 * 751 * @param route The route that was connected 752 * @hide 753 */ 754 public void onRouteConnected(Route route) { 755 } 756 757 /** 758 * Called when a route was disconnected. Further calls to the route will 759 * fail. If available a reason for being disconnected will be provided. 760 * <p> 761 * Valid reasons are: 762 * <ul> 763 * <li>{@link #DISCONNECT_REASON_USER_STOPPING}</li> 764 * <li>{@link #DISCONNECT_REASON_PROVIDER_DISCONNECTED}</li> 765 * <li>{@link #DISCONNECT_REASON_ROUTE_CHANGED}</li> 766 * <li>{@link #DISCONNECT_REASON_SESSION_DISCONNECTED}</li> 767 * <li>{@link #DISCONNECT_REASON_SESSION_DESTROYED}</li> 768 * </ul> 769 * 770 * @param route The route that disconnected 771 * @param reason The reason for the disconnect 772 * @hide 773 */ 774 public void onRouteDisconnected(Route route, int reason) { 775 } 776 } 777 778 /** 779 * Receives transport control commands. Callbacks may be registered using 780 * {@link #addTransportControlsCallback}. 781 */ 782 public static abstract class TransportControlsCallback { 783 784 /** 785 * Override to handle requests to begin playback. 786 */ 787 public void onPlay() { 788 } 789 790 /** 791 * Override to handle requests to pause playback. 792 */ 793 public void onPause() { 794 } 795 796 /** 797 * Override to handle requests to skip to the next media item. 798 */ 799 public void onSkipToNext() { 800 } 801 802 /** 803 * Override to handle requests to skip to the previous media item. 804 */ 805 public void onSkipToPrevious() { 806 } 807 808 /** 809 * Override to handle requests to fast forward. 810 */ 811 public void onFastForward() { 812 } 813 814 /** 815 * Override to handle requests to rewind. 816 */ 817 public void onRewind() { 818 } 819 820 /** 821 * Override to handle requests to stop playback. 822 */ 823 public void onStop() { 824 } 825 826 /** 827 * Override to handle requests to seek to a specific position in ms. 828 * 829 * @param pos New position to move to, in milliseconds. 830 */ 831 public void onSeekTo(long pos) { 832 } 833 834 /** 835 * Override to handle the item being rated. 836 * 837 * @param rating 838 */ 839 public void onSetRating(@NonNull Rating rating) { 840 } 841 842 /** 843 * Report that audio focus has changed on the app. This only happens if 844 * you have indicated you have started playing with 845 * {@link #setPlaybackState}. 846 * 847 * @param focusChange The type of focus change, TBD. 848 * @hide 849 */ 850 public void onRouteFocusChange(int focusChange) { 851 } 852 } 853 854 /** 855 * @hide 856 */ 857 public static class CallbackStub extends ISessionCallback.Stub { 858 private WeakReference<MediaSession> mMediaSession; 859 860 public void setMediaSession(MediaSession session) { 861 mMediaSession = new WeakReference<MediaSession>(session); 862 } 863 864 @Override 865 public void onCommand(String command, Bundle extras, ResultReceiver cb) 866 throws RemoteException { 867 MediaSession session = mMediaSession.get(); 868 if (session != null) { 869 session.postCommand(command, extras, cb); 870 } 871 } 872 873 @Override 874 public void onMediaButton(Intent mediaButtonIntent, int sequenceNumber, ResultReceiver cb) 875 throws RemoteException { 876 MediaSession session = mMediaSession.get(); 877 try { 878 if (session != null) { 879 session.postMediaButton(mediaButtonIntent); 880 } 881 } finally { 882 if (cb != null) { 883 cb.send(sequenceNumber, null); 884 } 885 } 886 } 887 888 @Override 889 public void onRequestRouteChange(RouteInfo route) throws RemoteException { 890 MediaSession session = mMediaSession.get(); 891 if (session != null) { 892 session.postRequestRouteChange(route); 893 } 894 } 895 896 @Override 897 public void onRouteConnected(RouteInfo route, RouteOptions options) { 898 MediaSession session = mMediaSession.get(); 899 if (session != null) { 900 session.postRouteConnected(route, options); 901 } 902 } 903 904 @Override 905 public void onRouteDisconnected(RouteInfo route, int reason) { 906 MediaSession session = mMediaSession.get(); 907 if (session != null) { 908 session.postRouteDisconnected(route, reason); 909 } 910 } 911 912 @Override 913 public void onPlay() throws RemoteException { 914 MediaSession session = mMediaSession.get(); 915 if (session != null) { 916 session.dispatchPlay(); 917 } 918 } 919 920 @Override 921 public void onPause() throws RemoteException { 922 MediaSession session = mMediaSession.get(); 923 if (session != null) { 924 session.dispatchPause(); 925 } 926 } 927 928 @Override 929 public void onStop() throws RemoteException { 930 MediaSession session = mMediaSession.get(); 931 if (session != null) { 932 session.dispatchStop(); 933 } 934 } 935 936 @Override 937 public void onNext() throws RemoteException { 938 MediaSession session = mMediaSession.get(); 939 if (session != null) { 940 session.dispatchNext(); 941 } 942 } 943 944 @Override 945 public void onPrevious() throws RemoteException { 946 MediaSession session = mMediaSession.get(); 947 if (session != null) { 948 session.dispatchPrevious(); 949 } 950 } 951 952 @Override 953 public void onFastForward() throws RemoteException { 954 MediaSession session = mMediaSession.get(); 955 if (session != null) { 956 session.dispatchFastForward(); 957 } 958 } 959 960 @Override 961 public void onRewind() throws RemoteException { 962 MediaSession session = mMediaSession.get(); 963 if (session != null) { 964 session.dispatchRewind(); 965 } 966 } 967 968 @Override 969 public void onSeekTo(long pos) throws RemoteException { 970 MediaSession session = mMediaSession.get(); 971 if (session != null) { 972 session.dispatchSeekTo(pos); 973 } 974 } 975 976 @Override 977 public void onRate(Rating rating) throws RemoteException { 978 MediaSession session = mMediaSession.get(); 979 if (session != null) { 980 session.dispatchRate(rating); 981 } 982 } 983 984 @Override 985 public void onRouteEvent(RouteEvent event) throws RemoteException { 986 MediaSession session = mMediaSession.get(); 987 if (session != null) { 988 RouteInterface.EventListener iface 989 = session.mInterfaceListeners.get(event.getIface()); 990 Log.d(TAG, "Received route event on iface " + event.getIface() + ". Listener is " 991 + iface); 992 if (iface != null) { 993 iface.onEvent(event.getEvent(), event.getExtras()); 994 } 995 } 996 } 997 998 @Override 999 public void onRouteStateChange(int state) throws RemoteException { 1000 // TODO 1001 } 1002 1003 @Override 1004 public void onAdjustVolumeBy(int delta) throws RemoteException { 1005 MediaSession session = mMediaSession.get(); 1006 if (session != null) { 1007 if (session.mVolumeProvider != null) { 1008 session.mVolumeProvider.onAdjustVolumeBy(delta); 1009 } 1010 } 1011 } 1012 1013 @Override 1014 public void onSetVolumeTo(int value) throws RemoteException { 1015 MediaSession session = mMediaSession.get(); 1016 if (session != null) { 1017 if (session.mVolumeProvider != null) { 1018 session.mVolumeProvider.onSetVolumeTo(value); 1019 } 1020 } 1021 } 1022 1023 } 1024 1025 private class CallbackMessageHandler extends Handler { 1026 private static final int MSG_MEDIA_BUTTON = 1; 1027 private static final int MSG_COMMAND = 2; 1028 private static final int MSG_ROUTE_CHANGE = 3; 1029 private static final int MSG_ROUTE_CONNECTED = 4; 1030 private static final int MSG_ROUTE_DISCONNECTED = 5; 1031 1032 private MediaSession.Callback mCallback; 1033 1034 public CallbackMessageHandler(Looper looper, MediaSession.Callback callback) { 1035 super(looper, null, true); 1036 mCallback = callback; 1037 } 1038 1039 @Override 1040 public void handleMessage(Message msg) { 1041 synchronized (mLock) { 1042 if (mCallback == null) { 1043 return; 1044 } 1045 switch (msg.what) { 1046 case MSG_MEDIA_BUTTON: 1047 mCallback.onMediaButtonEvent((Intent) msg.obj); 1048 break; 1049 case MSG_COMMAND: 1050 Command cmd = (Command) msg.obj; 1051 mCallback.onControlCommand(cmd.command, cmd.extras, cmd.stub); 1052 break; 1053 case MSG_ROUTE_CHANGE: 1054 mCallback.onRequestRouteChange((RouteInfo) msg.obj); 1055 break; 1056 case MSG_ROUTE_CONNECTED: 1057 mCallback.onRouteConnected((Route) msg.obj); 1058 break; 1059 case MSG_ROUTE_DISCONNECTED: 1060 mCallback.onRouteDisconnected((Route) msg.obj, msg.arg1); 1061 break; 1062 } 1063 } 1064 } 1065 1066 public void post(int what, Object obj) { 1067 obtainMessage(what, obj).sendToTarget(); 1068 } 1069 1070 public void post(int what, Object obj, int arg1) { 1071 obtainMessage(what, arg1, 0, obj).sendToTarget(); 1072 } 1073 } 1074 1075 private static final class Command { 1076 public final String command; 1077 public final Bundle extras; 1078 public final ResultReceiver stub; 1079 1080 public Command(String command, Bundle extras, ResultReceiver stub) { 1081 this.command = command; 1082 this.extras = extras; 1083 this.stub = stub; 1084 } 1085 } 1086 1087 private class TransportMessageHandler extends Handler { 1088 private static final int MSG_PLAY = 1; 1089 private static final int MSG_PAUSE = 2; 1090 private static final int MSG_STOP = 3; 1091 private static final int MSG_NEXT = 4; 1092 private static final int MSG_PREVIOUS = 5; 1093 private static final int MSG_FAST_FORWARD = 6; 1094 private static final int MSG_REWIND = 7; 1095 private static final int MSG_SEEK_TO = 8; 1096 private static final int MSG_RATE = 9; 1097 1098 private TransportControlsCallback mCallback; 1099 1100 public TransportMessageHandler(Looper looper, TransportControlsCallback cb) { 1101 super(looper); 1102 mCallback = cb; 1103 } 1104 1105 public void post(int what, Object obj) { 1106 obtainMessage(what, obj).sendToTarget(); 1107 } 1108 1109 public void post(int what) { 1110 post(what, null); 1111 } 1112 1113 @Override 1114 public void handleMessage(Message msg) { 1115 switch (msg.what) { 1116 case MSG_PLAY: 1117 mCallback.onPlay(); 1118 break; 1119 case MSG_PAUSE: 1120 mCallback.onPause(); 1121 break; 1122 case MSG_STOP: 1123 mCallback.onStop(); 1124 break; 1125 case MSG_NEXT: 1126 mCallback.onSkipToNext(); 1127 break; 1128 case MSG_PREVIOUS: 1129 mCallback.onSkipToPrevious(); 1130 break; 1131 case MSG_FAST_FORWARD: 1132 mCallback.onFastForward(); 1133 break; 1134 case MSG_REWIND: 1135 mCallback.onRewind(); 1136 break; 1137 case MSG_SEEK_TO: 1138 mCallback.onSeekTo((Long) msg.obj); 1139 break; 1140 case MSG_RATE: 1141 mCallback.onSetRating((Rating) msg.obj); 1142 break; 1143 } 1144 } 1145 } 1146} 1147