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