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