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