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