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