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