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