MediaSession.java revision 4dfedcb0c382e9094d129d6e3ad7c1cc99b0f9df
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 mVolumeProvider = volumeProvider; 308 volumeProvider.setCallback(new VolumeProvider.Callback() { 309 @Override 310 public void onVolumeChanged(VolumeProvider volumeProvider) { 311 notifyRemoteVolumeChanged(volumeProvider); 312 } 313 }); 314 315 try { 316 mBinder.setPlaybackToRemote(volumeProvider.getVolumeControl(), 317 volumeProvider.getMaxVolume()); 318 mBinder.setCurrentVolume(volumeProvider.getCurrentVolume()); 319 } catch (RemoteException e) { 320 Log.wtf(TAG, "Failure in setPlaybackToRemote.", e); 321 } 322 } 323 324 /** 325 * Set if this session is currently active and ready to receive commands. If 326 * set to false your session's controller may not be discoverable. You must 327 * set the session to active before it can start receiving media button 328 * events or transport commands. 329 * 330 * @param active Whether this session is active or not. 331 */ 332 public void setActive(boolean active) { 333 if (mActive == active) { 334 return; 335 } 336 try { 337 mBinder.setActive(active); 338 mActive = active; 339 } catch (RemoteException e) { 340 Log.wtf(TAG, "Failure in setActive.", e); 341 } 342 } 343 344 /** 345 * Get the current active state of this session. 346 * 347 * @return True if the session is active, false otherwise. 348 */ 349 public boolean isActive() { 350 return mActive; 351 } 352 353 /** 354 * Send a proprietary event to all MediaControllers listening to this 355 * Session. It's up to the Controller/Session owner to determine the meaning 356 * of any events. 357 * 358 * @param event The name of the event to send 359 * @param extras Any extras included with the event 360 */ 361 public void sendSessionEvent(@NonNull String event, @Nullable Bundle extras) { 362 if (TextUtils.isEmpty(event)) { 363 throw new IllegalArgumentException("event cannot be null or empty"); 364 } 365 try { 366 mBinder.sendEvent(event, extras); 367 } catch (RemoteException e) { 368 Log.wtf(TAG, "Error sending event", e); 369 } 370 } 371 372 /** 373 * This must be called when an app has finished performing playback. If 374 * playback is expected to start again shortly the session can be left open, 375 * but it must be released if your activity or service is being destroyed. 376 */ 377 public void release() { 378 try { 379 mBinder.destroy(); 380 } catch (RemoteException e) { 381 Log.wtf(TAG, "Error releasing session: ", e); 382 } 383 } 384 385 /** 386 * Retrieve a token object that can be used by apps to create a 387 * {@link MediaController} for interacting with this session. The owner of 388 * the session is responsible for deciding how to distribute these tokens. 389 * 390 * @return A token that can be used to create a MediaController for this 391 * session 392 */ 393 public @NonNull Token getSessionToken() { 394 return mSessionToken; 395 } 396 397 /** 398 * Get a controller for this session. This is a convenience method to avoid 399 * having to cache your own controller in process. 400 * 401 * @return A controller for this session. 402 */ 403 public @NonNull MediaController getController() { 404 return mController; 405 } 406 407 /** 408 * Update the current playback state. 409 * 410 * @param state The current state of playback 411 */ 412 public void setPlaybackState(@Nullable PlaybackState state) { 413 mPlaybackState = state; 414 try { 415 mBinder.setPlaybackState(state); 416 } catch (RemoteException e) { 417 Log.wtf(TAG, "Dead object in setPlaybackState.", e); 418 } 419 } 420 421 /** 422 * Update the current metadata. New metadata can be created using 423 * {@link android.media.MediaMetadata.Builder}. 424 * 425 * @param metadata The new metadata 426 */ 427 public void setMetadata(@Nullable MediaMetadata metadata) { 428 try { 429 mBinder.setMetadata(metadata); 430 } catch (RemoteException e) { 431 Log.wtf(TAG, "Dead object in setPlaybackState.", e); 432 } 433 } 434 435 /** 436 * Update the list of items in the play queue. It is an ordered list and 437 * should contain the current item, and previous or upcoming items if they 438 * exist. Specify null if there is no current play queue. 439 * <p> 440 * The queue should be of reasonable size. If the play queue is unbounded 441 * within your app, it is better to send a reasonable amount in a sliding 442 * window instead. 443 * 444 * @param queue A list of items in the play queue. 445 */ 446 public void setQueue(@Nullable List<QueueItem> queue) { 447 try { 448 mBinder.setQueue(new ParceledListSlice<QueueItem>(queue)); 449 } catch (RemoteException e) { 450 Log.wtf("Dead object in setQueue.", e); 451 } 452 } 453 454 /** 455 * Set the title of the play queue. The UI should display this title along 456 * with the play queue itself. 457 * e.g. "Play Queue", "Now Playing", or an album name. 458 * 459 * @param title The title of the play queue. 460 */ 461 public void setQueueTitle(@Nullable CharSequence title) { 462 try { 463 mBinder.setQueueTitle(title); 464 } catch (RemoteException e) { 465 Log.wtf("Dead object in setQueueTitle.", e); 466 } 467 } 468 469 /** 470 * Set some extras that can be associated with the {@link MediaSession}. No assumptions should 471 * be made as to how a {@link MediaController} will handle these extras. 472 * Keys should be fully qualified (e.g. com.example.MY_EXTRA) to avoid conflicts. 473 * 474 * @param extras The extras associated with the {@link MediaSession}. 475 */ 476 public void setExtras(@Nullable Bundle extras) { 477 try { 478 mBinder.setExtras(extras); 479 } catch (RemoteException e) { 480 Log.wtf("Dead object in setExtras.", e); 481 } 482 } 483 484 /** 485 * Notify the system that the remote volume changed. 486 * 487 * @param provider The provider that is handling volume changes. 488 * @hide 489 */ 490 public void notifyRemoteVolumeChanged(VolumeProvider provider) { 491 if (provider == null || provider != mVolumeProvider) { 492 Log.w(TAG, "Received update from stale volume provider"); 493 return; 494 } 495 try { 496 mBinder.setCurrentVolume(provider.getCurrentVolume()); 497 } catch (RemoteException e) { 498 Log.e(TAG, "Error in notifyVolumeChanged", e); 499 } 500 } 501 502 private void dispatchPlay() { 503 postToCallback(CallbackMessageHandler.MSG_PLAY); 504 } 505 506 private void dispatchPlayFromMediaId(String mediaId, Bundle extras) { 507 postToCallback(CallbackMessageHandler.MSG_PLAY_MEDIA_ID, mediaId, extras); 508 } 509 510 private void dispatchPlayFromSearch(String query, Bundle extras) { 511 postToCallback(CallbackMessageHandler.MSG_PLAY_SEARCH, query, extras); 512 } 513 514 private void dispatchSkipToItem(long id) { 515 postToCallback(CallbackMessageHandler.MSG_SKIP_TO_ITEM, id); 516 } 517 518 private void dispatchPause() { 519 postToCallback(CallbackMessageHandler.MSG_PAUSE); 520 } 521 522 private void dispatchStop() { 523 postToCallback(CallbackMessageHandler.MSG_STOP); 524 } 525 526 private void dispatchNext() { 527 postToCallback(CallbackMessageHandler.MSG_NEXT); 528 } 529 530 private void dispatchPrevious() { 531 postToCallback(CallbackMessageHandler.MSG_PREVIOUS); 532 } 533 534 private void dispatchFastForward() { 535 postToCallback(CallbackMessageHandler.MSG_FAST_FORWARD); 536 } 537 538 private void dispatchRewind() { 539 postToCallback(CallbackMessageHandler.MSG_REWIND); 540 } 541 542 private void dispatchSeekTo(long pos) { 543 postToCallback(CallbackMessageHandler.MSG_SEEK_TO, pos); 544 } 545 546 private void dispatchRate(Rating rating) { 547 postToCallback(CallbackMessageHandler.MSG_RATE, rating); 548 } 549 550 private void dispatchCustomAction(String action, Bundle args) { 551 postToCallback(CallbackMessageHandler.MSG_CUSTOM_ACTION, action, args); 552 } 553 554 private void dispatchMediaButton(Intent mediaButtonIntent) { 555 postToCallback(CallbackMessageHandler.MSG_MEDIA_BUTTON, mediaButtonIntent); 556 } 557 558 private void postToCallback(int what) { 559 postToCallback(what, null); 560 } 561 562 private void postCommand(String command, Bundle args, ResultReceiver resultCb) { 563 Command cmd = new Command(command, args, resultCb); 564 postToCallback(CallbackMessageHandler.MSG_COMMAND, cmd); 565 } 566 567 private void postToCallback(int what, Object obj) { 568 postToCallback(what, obj, null); 569 } 570 571 private void postToCallback(int what, Object obj, Bundle extras) { 572 synchronized (mLock) { 573 if (mCallback != null) { 574 mCallback.post(what, obj, extras); 575 } 576 } 577 } 578 579 /** 580 * Return true if this is considered an active playback state. 581 * 582 * @hide 583 */ 584 public static boolean isActiveState(int state) { 585 switch (state) { 586 case PlaybackState.STATE_FAST_FORWARDING: 587 case PlaybackState.STATE_REWINDING: 588 case PlaybackState.STATE_SKIPPING_TO_PREVIOUS: 589 case PlaybackState.STATE_SKIPPING_TO_NEXT: 590 case PlaybackState.STATE_BUFFERING: 591 case PlaybackState.STATE_CONNECTING: 592 case PlaybackState.STATE_PLAYING: 593 return true; 594 } 595 return false; 596 } 597 598 /** 599 * Represents an ongoing session. This may be passed to apps by the session 600 * owner to allow them to create a {@link MediaController} to communicate with 601 * the session. 602 */ 603 public static final class Token implements Parcelable { 604 605 private ISessionController mBinder; 606 607 /** 608 * @hide 609 */ 610 public Token(ISessionController binder) { 611 mBinder = binder; 612 } 613 614 @Override 615 public int describeContents() { 616 return 0; 617 } 618 619 @Override 620 public void writeToParcel(Parcel dest, int flags) { 621 dest.writeStrongBinder(mBinder.asBinder()); 622 } 623 624 @Override 625 public int hashCode() { 626 final int prime = 31; 627 int result = 1; 628 result = prime * result + ((mBinder == null) ? 0 : mBinder.asBinder().hashCode()); 629 return result; 630 } 631 632 @Override 633 public boolean equals(Object obj) { 634 if (this == obj) 635 return true; 636 if (obj == null) 637 return false; 638 if (getClass() != obj.getClass()) 639 return false; 640 Token other = (Token) obj; 641 if (mBinder == null) { 642 if (other.mBinder != null) 643 return false; 644 } else if (!mBinder.asBinder().equals(other.mBinder.asBinder())) 645 return false; 646 return true; 647 } 648 649 ISessionController getBinder() { 650 return mBinder; 651 } 652 653 public static final Parcelable.Creator<Token> CREATOR 654 = new Parcelable.Creator<Token>() { 655 @Override 656 public Token createFromParcel(Parcel in) { 657 return new Token(ISessionController.Stub.asInterface(in.readStrongBinder())); 658 } 659 660 @Override 661 public Token[] newArray(int size) { 662 return new Token[size]; 663 } 664 }; 665 } 666 667 /** 668 * Receives media buttons, transport controls, and commands from controllers 669 * and the system. A callback may be set using {@link #setCallback}. 670 */ 671 public abstract static class Callback { 672 private MediaSession mSession; 673 674 public Callback() { 675 } 676 677 /** 678 * Called when a controller has sent a command to this session. 679 * The owner of the session may handle custom commands but is not 680 * required to. 681 * 682 * @param command The command name. 683 * @param args Optional parameters for the command, may be null. 684 * @param cb A result receiver to which a result may be sent by the command, may be null. 685 */ 686 public void onCommand(@NonNull String command, @Nullable Bundle args, 687 @Nullable ResultReceiver cb) { 688 } 689 690 /** 691 * Called when a media button is pressed and this session has the 692 * highest priority or a controller sends a media button event to the 693 * session. The default behavior will call the relevant method if the 694 * action for it was set. 695 * <p> 696 * The intent will be of type {@link Intent#ACTION_MEDIA_BUTTON} with a 697 * KeyEvent in {@link Intent#EXTRA_KEY_EVENT} 698 * 699 * @param mediaButtonIntent an intent containing the KeyEvent as an 700 * extra 701 * @return True if the event was handled, false otherwise. 702 */ 703 public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) { 704 if (mSession != null 705 && Intent.ACTION_MEDIA_BUTTON.equals(mediaButtonIntent.getAction())) { 706 KeyEvent ke = mediaButtonIntent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); 707 if (ke != null && ke.getAction() == KeyEvent.ACTION_DOWN) { 708 PlaybackState state = mSession.mPlaybackState; 709 long validActions = state == null ? 0 : state.getActions(); 710 switch (ke.getKeyCode()) { 711 case KeyEvent.KEYCODE_MEDIA_PLAY: 712 if ((validActions & PlaybackState.ACTION_PLAY) != 0) { 713 onPlay(); 714 return true; 715 } 716 break; 717 case KeyEvent.KEYCODE_MEDIA_PAUSE: 718 if ((validActions & PlaybackState.ACTION_PAUSE) != 0) { 719 onPause(); 720 return true; 721 } 722 break; 723 case KeyEvent.KEYCODE_MEDIA_NEXT: 724 if ((validActions & PlaybackState.ACTION_SKIP_TO_NEXT) != 0) { 725 onSkipToNext(); 726 return true; 727 } 728 break; 729 case KeyEvent.KEYCODE_MEDIA_PREVIOUS: 730 if ((validActions & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0) { 731 onSkipToPrevious(); 732 return true; 733 } 734 break; 735 case KeyEvent.KEYCODE_MEDIA_STOP: 736 if ((validActions & PlaybackState.ACTION_STOP) != 0) { 737 onStop(); 738 return true; 739 } 740 break; 741 case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: 742 if ((validActions & PlaybackState.ACTION_FAST_FORWARD) != 0) { 743 onFastForward(); 744 return true; 745 } 746 break; 747 case KeyEvent.KEYCODE_MEDIA_REWIND: 748 if ((validActions & PlaybackState.ACTION_REWIND) != 0) { 749 onRewind(); 750 return true; 751 } 752 break; 753 case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: 754 case KeyEvent.KEYCODE_HEADSETHOOK: 755 boolean isPlaying = state == null ? false 756 : state.getState() == PlaybackState.STATE_PLAYING; 757 boolean canPlay = (validActions & (PlaybackState.ACTION_PLAY_PAUSE 758 | PlaybackState.ACTION_PLAY)) != 0; 759 boolean canPause = (validActions & (PlaybackState.ACTION_PLAY_PAUSE 760 | PlaybackState.ACTION_PAUSE)) != 0; 761 if (isPlaying && canPause) { 762 onPause(); 763 return true; 764 } else if (!isPlaying && canPlay) { 765 onPlay(); 766 return true; 767 } 768 break; 769 } 770 } 771 } 772 return false; 773 } 774 775 /** 776 * Override to handle requests to begin playback. 777 */ 778 public void onPlay() { 779 } 780 781 /** 782 * Override to handle requests to play a specific mediaId that was 783 * provided by your app's {@link MediaBrowserService}. 784 */ 785 public void onPlayFromMediaId(String mediaId, Bundle extras) { 786 } 787 788 /** 789 * Override to handle requests to begin playback from a search query. An 790 * empty query indicates that the app may play any music. The 791 * implementation should attempt to make a smart choice about what to 792 * play. 793 */ 794 public void onPlayFromSearch(String query, Bundle extras) { 795 } 796 797 /** 798 * Override to handle requests to play an item with a given id from the 799 * play queue. 800 */ 801 public void onSkipToQueueItem(long id) { 802 } 803 804 /** 805 * Override to handle requests to pause playback. 806 */ 807 public void onPause() { 808 } 809 810 /** 811 * Override to handle requests to skip to the next media item. 812 */ 813 public void onSkipToNext() { 814 } 815 816 /** 817 * Override to handle requests to skip to the previous media item. 818 */ 819 public void onSkipToPrevious() { 820 } 821 822 /** 823 * Override to handle requests to fast forward. 824 */ 825 public void onFastForward() { 826 } 827 828 /** 829 * Override to handle requests to rewind. 830 */ 831 public void onRewind() { 832 } 833 834 /** 835 * Override to handle requests to stop playback. 836 */ 837 public void onStop() { 838 } 839 840 /** 841 * Override to handle requests to seek to a specific position in ms. 842 * 843 * @param pos New position to move to, in milliseconds. 844 */ 845 public void onSeekTo(long pos) { 846 } 847 848 /** 849 * Override to handle the item being rated. 850 * 851 * @param rating 852 */ 853 public void onSetRating(@NonNull Rating rating) { 854 } 855 856 /** 857 * Called when a {@link MediaController} wants a {@link PlaybackState.CustomAction} to be 858 * performed. 859 * 860 * @param action The action that was originally sent in the 861 * {@link PlaybackState.CustomAction}. 862 * @param extras Optional extras specified by the {@link MediaController}. 863 */ 864 public void onCustomAction(@NonNull String action, @Nullable Bundle extras) { 865 } 866 } 867 868 /** 869 * @hide 870 */ 871 public static class CallbackStub extends ISessionCallback.Stub { 872 private WeakReference<MediaSession> mMediaSession; 873 874 public CallbackStub(MediaSession session) { 875 mMediaSession = new WeakReference<MediaSession>(session); 876 } 877 878 @Override 879 public void onCommand(String command, Bundle args, ResultReceiver cb) { 880 MediaSession session = mMediaSession.get(); 881 if (session != null) { 882 session.postCommand(command, args, cb); 883 } 884 } 885 886 @Override 887 public void onMediaButton(Intent mediaButtonIntent, int sequenceNumber, 888 ResultReceiver cb) { 889 MediaSession session = mMediaSession.get(); 890 try { 891 if (session != null) { 892 session.dispatchMediaButton(mediaButtonIntent); 893 } 894 } finally { 895 if (cb != null) { 896 cb.send(sequenceNumber, null); 897 } 898 } 899 } 900 901 @Override 902 public void onPlay() { 903 MediaSession session = mMediaSession.get(); 904 if (session != null) { 905 session.dispatchPlay(); 906 } 907 } 908 909 @Override 910 public void onPlayFromMediaId(String mediaId, Bundle extras) { 911 MediaSession session = mMediaSession.get(); 912 if (session != null) { 913 session.dispatchPlayFromMediaId(mediaId, extras); 914 } 915 } 916 917 @Override 918 public void onPlayFromSearch(String query, Bundle extras) { 919 MediaSession session = mMediaSession.get(); 920 if (session != null) { 921 session.dispatchPlayFromSearch(query, extras); 922 } 923 } 924 925 @Override 926 public void onSkipToTrack(long id) { 927 MediaSession session = mMediaSession.get(); 928 if (session != null) { 929 session.dispatchSkipToItem(id); 930 } 931 } 932 933 @Override 934 public void onPause() { 935 MediaSession session = mMediaSession.get(); 936 if (session != null) { 937 session.dispatchPause(); 938 } 939 } 940 941 @Override 942 public void onStop() { 943 MediaSession session = mMediaSession.get(); 944 if (session != null) { 945 session.dispatchStop(); 946 } 947 } 948 949 @Override 950 public void onNext() { 951 MediaSession session = mMediaSession.get(); 952 if (session != null) { 953 session.dispatchNext(); 954 } 955 } 956 957 @Override 958 public void onPrevious() { 959 MediaSession session = mMediaSession.get(); 960 if (session != null) { 961 session.dispatchPrevious(); 962 } 963 } 964 965 @Override 966 public void onFastForward() { 967 MediaSession session = mMediaSession.get(); 968 if (session != null) { 969 session.dispatchFastForward(); 970 } 971 } 972 973 @Override 974 public void onRewind() { 975 MediaSession session = mMediaSession.get(); 976 if (session != null) { 977 session.dispatchRewind(); 978 } 979 } 980 981 @Override 982 public void onSeekTo(long pos) { 983 MediaSession session = mMediaSession.get(); 984 if (session != null) { 985 session.dispatchSeekTo(pos); 986 } 987 } 988 989 @Override 990 public void onRate(Rating rating) { 991 MediaSession session = mMediaSession.get(); 992 if (session != null) { 993 session.dispatchRate(rating); 994 } 995 } 996 997 @Override 998 public void onCustomAction(String action, Bundle args) { 999 MediaSession session = mMediaSession.get(); 1000 if (session != null) { 1001 session.dispatchCustomAction(action, args); 1002 } 1003 } 1004 1005 @Override 1006 public void onAdjustVolume(int direction) { 1007 MediaSession session = mMediaSession.get(); 1008 if (session != null) { 1009 if (session.mVolumeProvider != null) { 1010 session.mVolumeProvider.onAdjustVolume(direction); 1011 } 1012 } 1013 } 1014 1015 @Override 1016 public void onSetVolumeTo(int value) { 1017 MediaSession session = mMediaSession.get(); 1018 if (session != null) { 1019 if (session.mVolumeProvider != null) { 1020 session.mVolumeProvider.onSetVolumeTo(value); 1021 } 1022 } 1023 } 1024 1025 } 1026 1027 /** 1028 * A single item that is part of the play queue. It contains a description 1029 * of the item and its id in the queue. 1030 */ 1031 public static final class QueueItem implements Parcelable { 1032 /** 1033 * This id is reserved. No items can be explicitly asigned this id. 1034 */ 1035 public static final int UNKNOWN_ID = -1; 1036 1037 private final MediaDescription mDescription; 1038 private final long mId; 1039 1040 /** 1041 * Create a new {@link MediaSession.QueueItem}. 1042 * 1043 * @param description The {@link MediaDescription} for this item. 1044 * @param id An identifier for this item. It must be unique within the 1045 * play queue and cannot be {@link #UNKNOWN_ID}. 1046 */ 1047 public QueueItem(MediaDescription description, long id) { 1048 if (description == null) { 1049 throw new IllegalArgumentException("Description cannot be null."); 1050 } 1051 if (id == UNKNOWN_ID) { 1052 throw new IllegalArgumentException("Id cannot be QueueItem.UNKNOWN_ID"); 1053 } 1054 mDescription = description; 1055 mId = id; 1056 } 1057 1058 private QueueItem(Parcel in) { 1059 mDescription = MediaDescription.CREATOR.createFromParcel(in); 1060 mId = in.readLong(); 1061 } 1062 1063 /** 1064 * Get the description for this item. 1065 */ 1066 public MediaDescription getDescription() { 1067 return mDescription; 1068 } 1069 1070 /** 1071 * Get the queue id for this item. 1072 */ 1073 public long getQueueId() { 1074 return mId; 1075 } 1076 1077 @Override 1078 public void writeToParcel(Parcel dest, int flags) { 1079 mDescription.writeToParcel(dest, flags); 1080 dest.writeLong(mId); 1081 } 1082 1083 @Override 1084 public int describeContents() { 1085 return 0; 1086 } 1087 1088 public static final Creator<MediaSession.QueueItem> CREATOR = new Creator<MediaSession.QueueItem>() { 1089 1090 @Override 1091 public MediaSession.QueueItem createFromParcel(Parcel p) { 1092 return new MediaSession.QueueItem(p); 1093 } 1094 1095 @Override 1096 public MediaSession.QueueItem[] newArray(int size) { 1097 return new MediaSession.QueueItem[size]; 1098 } 1099 }; 1100 1101 @Override 1102 public String toString() { 1103 return "MediaSession.QueueItem {" + 1104 "Description=" + mDescription + 1105 ", Id=" + mId + " }"; 1106 } 1107 } 1108 1109 private static final class Command { 1110 public final String command; 1111 public final Bundle extras; 1112 public final ResultReceiver stub; 1113 1114 public Command(String command, Bundle extras, ResultReceiver stub) { 1115 this.command = command; 1116 this.extras = extras; 1117 this.stub = stub; 1118 } 1119 } 1120 1121 private class CallbackMessageHandler extends Handler { 1122 1123 private static final int MSG_PLAY = 1; 1124 private static final int MSG_PLAY_MEDIA_ID = 2; 1125 private static final int MSG_PLAY_SEARCH = 3; 1126 private static final int MSG_SKIP_TO_ITEM = 4; 1127 private static final int MSG_PAUSE = 5; 1128 private static final int MSG_STOP = 6; 1129 private static final int MSG_NEXT = 7; 1130 private static final int MSG_PREVIOUS = 8; 1131 private static final int MSG_FAST_FORWARD = 9; 1132 private static final int MSG_REWIND = 10; 1133 private static final int MSG_SEEK_TO = 11; 1134 private static final int MSG_RATE = 12; 1135 private static final int MSG_CUSTOM_ACTION = 13; 1136 private static final int MSG_MEDIA_BUTTON = 14; 1137 private static final int MSG_COMMAND = 15; 1138 1139 private MediaSession.Callback mCallback; 1140 1141 public CallbackMessageHandler(Looper looper, MediaSession.Callback callback) { 1142 super(looper, null, true); 1143 mCallback = callback; 1144 } 1145 1146 public void post(int what, Object obj, Bundle bundle) { 1147 Message msg = obtainMessage(what, obj); 1148 msg.setData(bundle); 1149 msg.sendToTarget(); 1150 } 1151 1152 public void post(int what, Object obj) { 1153 obtainMessage(what, obj).sendToTarget(); 1154 } 1155 1156 public void post(int what) { 1157 post(what, null); 1158 } 1159 1160 public void post(int what, Object obj, int arg1) { 1161 obtainMessage(what, arg1, 0, obj).sendToTarget(); 1162 } 1163 1164 @Override 1165 public void handleMessage(Message msg) { 1166 switch (msg.what) { 1167 case MSG_PLAY: 1168 mCallback.onPlay(); 1169 break; 1170 case MSG_PLAY_MEDIA_ID: 1171 mCallback.onPlayFromMediaId((String) msg.obj, msg.getData()); 1172 break; 1173 case MSG_PLAY_SEARCH: 1174 mCallback.onPlayFromSearch((String) msg.obj, msg.getData()); 1175 break; 1176 case MSG_SKIP_TO_ITEM: 1177 mCallback.onSkipToQueueItem((Long) msg.obj); 1178 case MSG_PAUSE: 1179 mCallback.onPause(); 1180 break; 1181 case MSG_STOP: 1182 mCallback.onStop(); 1183 break; 1184 case MSG_NEXT: 1185 mCallback.onSkipToNext(); 1186 break; 1187 case MSG_PREVIOUS: 1188 mCallback.onSkipToPrevious(); 1189 break; 1190 case MSG_FAST_FORWARD: 1191 mCallback.onFastForward(); 1192 break; 1193 case MSG_REWIND: 1194 mCallback.onRewind(); 1195 break; 1196 case MSG_SEEK_TO: 1197 mCallback.onSeekTo((Long) msg.obj); 1198 break; 1199 case MSG_RATE: 1200 mCallback.onSetRating((Rating) msg.obj); 1201 break; 1202 case MSG_CUSTOM_ACTION: 1203 mCallback.onCustomAction((String) msg.obj, msg.getData()); 1204 break; 1205 case MSG_MEDIA_BUTTON: 1206 mCallback.onMediaButtonEvent((Intent) msg.obj); 1207 break; 1208 case MSG_COMMAND: 1209 Command cmd = (Command) msg.obj; 1210 mCallback.onCommand(cmd.command, cmd.extras, cmd.stub); 1211 break; 1212 } 1213 } 1214 } 1215} 1216