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