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