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.IntRange; 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.annotation.SystemApi; 24 import android.app.PendingIntent; 25 import android.compat.annotation.UnsupportedAppUsage; 26 import android.content.Context; 27 import android.content.pm.ParceledListSlice; 28 import android.media.AudioAttributes; 29 import android.media.AudioManager; 30 import android.media.MediaMetadata; 31 import android.media.Rating; 32 import android.media.VolumeProvider; 33 import android.media.VolumeProvider.ControlType; 34 import android.media.session.MediaSession.QueueItem; 35 import android.net.Uri; 36 import android.os.Build; 37 import android.os.Bundle; 38 import android.os.Handler; 39 import android.os.Looper; 40 import android.os.Message; 41 import android.os.Parcel; 42 import android.os.Parcelable; 43 import android.os.RemoteException; 44 import android.os.ResultReceiver; 45 import android.text.TextUtils; 46 import android.util.Log; 47 import android.view.KeyEvent; 48 49 import com.android.internal.annotations.VisibleForTesting; 50 51 import java.lang.annotation.Retention; 52 import java.lang.annotation.RetentionPolicy; 53 import java.lang.ref.WeakReference; 54 import java.util.ArrayList; 55 import java.util.List; 56 57 /** 58 * Allows an app to interact with an ongoing media session. Media buttons and 59 * other commands can be sent to the session. A callback may be registered to 60 * receive updates from the session, such as metadata and play state changes. 61 * <p> 62 * A MediaController can be created through {@link MediaSessionManager} if you 63 * hold the "android.permission.MEDIA_CONTENT_CONTROL" permission or are an 64 * enabled notification listener or by getting a {@link MediaSession.Token} 65 * directly from the session owner. 66 * <p> 67 * MediaController objects are thread-safe. 68 */ 69 public final class MediaController { 70 private static final String TAG = "MediaController"; 71 72 private static final int MSG_EVENT = 1; 73 private static final int MSG_UPDATE_PLAYBACK_STATE = 2; 74 private static final int MSG_UPDATE_METADATA = 3; 75 private static final int MSG_UPDATE_VOLUME = 4; 76 private static final int MSG_UPDATE_QUEUE = 5; 77 private static final int MSG_UPDATE_QUEUE_TITLE = 6; 78 private static final int MSG_UPDATE_EXTRAS = 7; 79 private static final int MSG_DESTROYED = 8; 80 81 private final ISessionController mSessionBinder; 82 83 private final MediaSession.Token mToken; 84 private final Context mContext; 85 private final CallbackStub mCbStub = new CallbackStub(this); 86 private final ArrayList<MessageHandler> mCallbacks = new ArrayList<MessageHandler>(); 87 private final Object mLock = new Object(); 88 89 private boolean mCbRegistered = false; 90 private String mPackageName; 91 private String mTag; 92 private Bundle mSessionInfo; 93 94 private final TransportControls mTransportControls; 95 96 /** 97 * Create a new MediaController from a session's token. 98 * 99 * @param context The caller's context. 100 * @param token The token for the session. 101 */ MediaController(@onNull Context context, @NonNull MediaSession.Token token)102 public MediaController(@NonNull Context context, @NonNull MediaSession.Token token) { 103 if (context == null) { 104 throw new IllegalArgumentException("context shouldn't be null"); 105 } 106 if (token == null) { 107 throw new IllegalArgumentException("token shouldn't be null"); 108 } 109 if (token.getBinder() == null) { 110 throw new IllegalArgumentException("token.getBinder() shouldn't be null"); 111 } 112 mSessionBinder = token.getBinder(); 113 mTransportControls = new TransportControls(); 114 mToken = token; 115 mContext = context; 116 } 117 118 /** 119 * Get a {@link TransportControls} instance to send transport actions to 120 * the associated session. 121 * 122 * @return A transport controls instance. 123 */ getTransportControls()124 public @NonNull TransportControls getTransportControls() { 125 return mTransportControls; 126 } 127 128 /** 129 * Send the specified media button event to the session. Only media keys can 130 * be sent by this method, other keys will be ignored. 131 * 132 * @param keyEvent The media button event to dispatch. 133 * @return true if the event was sent to the session, false otherwise. 134 */ dispatchMediaButtonEvent(@onNull KeyEvent keyEvent)135 public boolean dispatchMediaButtonEvent(@NonNull KeyEvent keyEvent) { 136 if (keyEvent == null) { 137 throw new IllegalArgumentException("KeyEvent may not be null"); 138 } 139 if (!KeyEvent.isMediaSessionKey(keyEvent.getKeyCode())) { 140 return false; 141 } 142 try { 143 return mSessionBinder.sendMediaButton(mContext.getPackageName(), keyEvent); 144 } catch (RemoteException ex) { 145 throw ex.rethrowFromSystemServer(); 146 } 147 } 148 149 /** 150 * Get the current playback state for this session. 151 * 152 * @return The current PlaybackState or null 153 */ getPlaybackState()154 public @Nullable PlaybackState getPlaybackState() { 155 try { 156 return mSessionBinder.getPlaybackState(); 157 } catch (RemoteException ex) { 158 throw ex.rethrowFromSystemServer(); 159 } 160 } 161 162 /** 163 * Get the current metadata for this session. 164 * 165 * @return The current MediaMetadata or null. 166 */ getMetadata()167 public @Nullable MediaMetadata getMetadata() { 168 try { 169 return mSessionBinder.getMetadata(); 170 } catch (RemoteException ex) { 171 throw ex.rethrowFromSystemServer(); 172 } 173 } 174 175 /** 176 * Get the current play queue for this session if one is set. If you only 177 * care about the current item {@link #getMetadata()} should be used. 178 * 179 * @return The current play queue or null. 180 */ getQueue()181 public @Nullable List<MediaSession.QueueItem> getQueue() { 182 try { 183 ParceledListSlice list = mSessionBinder.getQueue(); 184 return list == null ? null : list.getList(); 185 } catch (RemoteException ex) { 186 throw ex.rethrowFromSystemServer(); 187 } 188 } 189 190 /** 191 * Get the queue title for this session. 192 */ getQueueTitle()193 public @Nullable CharSequence getQueueTitle() { 194 try { 195 return mSessionBinder.getQueueTitle(); 196 } catch (RemoteException ex) { 197 throw ex.rethrowFromSystemServer(); 198 } 199 } 200 201 /** 202 * Get the extras for this session. 203 */ getExtras()204 public @Nullable Bundle getExtras() { 205 try { 206 return mSessionBinder.getExtras(); 207 } catch (RemoteException ex) { 208 throw ex.rethrowFromSystemServer(); 209 } 210 } 211 212 /** 213 * Get the rating type supported by the session. One of: 214 * <ul> 215 * <li>{@link Rating#RATING_NONE}</li> 216 * <li>{@link Rating#RATING_HEART}</li> 217 * <li>{@link Rating#RATING_THUMB_UP_DOWN}</li> 218 * <li>{@link Rating#RATING_3_STARS}</li> 219 * <li>{@link Rating#RATING_4_STARS}</li> 220 * <li>{@link Rating#RATING_5_STARS}</li> 221 * <li>{@link Rating#RATING_PERCENTAGE}</li> 222 * </ul> 223 * 224 * @return The supported rating type 225 */ getRatingType()226 public int getRatingType() { 227 try { 228 return mSessionBinder.getRatingType(); 229 } catch (RemoteException ex) { 230 throw ex.rethrowFromSystemServer(); 231 } 232 } 233 234 /** 235 * Get the flags for this session. Flags are defined in {@link MediaSession}. 236 * 237 * @return The current set of flags for the session. 238 */ getFlags()239 public long getFlags() { 240 try { 241 return mSessionBinder.getFlags(); 242 } catch (RemoteException ex) { 243 throw ex.rethrowFromSystemServer(); 244 } 245 } 246 247 /** Returns the current playback info for this session. */ 248 @NonNull getPlaybackInfo()249 public PlaybackInfo getPlaybackInfo() { 250 try { 251 return mSessionBinder.getVolumeAttributes(); 252 } catch (RemoteException ex) { 253 throw ex.rethrowFromSystemServer(); 254 } 255 } 256 257 /** 258 * Get an intent for launching UI associated with this session if one 259 * exists. 260 * 261 * @return A {@link PendingIntent} to launch UI or null. 262 */ getSessionActivity()263 public @Nullable PendingIntent getSessionActivity() { 264 try { 265 return mSessionBinder.getLaunchPendingIntent(); 266 } catch (RemoteException ex) { 267 throw ex.rethrowFromSystemServer(); 268 } 269 } 270 271 /** 272 * Get the token for the session this is connected to. 273 * 274 * @return The token for the connected session. 275 */ getSessionToken()276 public @NonNull MediaSession.Token getSessionToken() { 277 return mToken; 278 } 279 280 /** 281 * Set the volume of the output this session is playing on. The command will 282 * be ignored if it does not support 283 * {@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}. The flags in 284 * {@link AudioManager} may be used to affect the handling. 285 * 286 * @see #getPlaybackInfo() 287 * @param value The value to set it to, between 0 and the reported max. 288 * @param flags Flags from {@link AudioManager} to include with the volume 289 * request. 290 */ setVolumeTo(int value, int flags)291 public void setVolumeTo(int value, int flags) { 292 try { 293 // Note: Need both package name and OP package name. Package name is used for 294 // RemoteUserInfo, and OP package name is used for AudioService's internal 295 // AppOpsManager usages. 296 mSessionBinder.setVolumeTo(mContext.getPackageName(), mContext.getOpPackageName(), 297 value, flags); 298 } catch (RemoteException ex) { 299 throw ex.rethrowFromSystemServer(); 300 } 301 } 302 303 /** 304 * Adjust the volume of the output this session is playing on. The direction 305 * must be one of {@link AudioManager#ADJUST_LOWER}, 306 * {@link AudioManager#ADJUST_RAISE}, or {@link AudioManager#ADJUST_SAME}. 307 * The command will be ignored if the session does not support 308 * {@link VolumeProvider#VOLUME_CONTROL_RELATIVE} or 309 * {@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}. The flags in 310 * {@link AudioManager} may be used to affect the handling. 311 * 312 * @see #getPlaybackInfo() 313 * @param direction The direction to adjust the volume in. 314 * @param flags Any flags to pass with the command. 315 */ adjustVolume(int direction, int flags)316 public void adjustVolume(int direction, int flags) { 317 try { 318 // Note: Need both package name and OP package name. Package name is used for 319 // RemoteUserInfo, and OP package name is used for AudioService's internal 320 // AppOpsManager usages. 321 mSessionBinder.adjustVolume(mContext.getPackageName(), mContext.getOpPackageName(), 322 direction, flags); 323 } catch (RemoteException ex) { 324 throw ex.rethrowFromSystemServer(); 325 } 326 } 327 328 /** 329 * Registers a callback to receive updates from the Session. Updates will be 330 * posted on the caller's thread. 331 * 332 * @param callback The callback object, must not be null. 333 */ registerCallback(@onNull Callback callback)334 public void registerCallback(@NonNull Callback callback) { 335 registerCallback(callback, null); 336 } 337 338 /** 339 * Registers a callback to receive updates from the session. Updates will be 340 * posted on the specified handler's thread. 341 * 342 * @param callback The callback object, must not be null. 343 * @param handler The handler to post updates on. If null the callers thread 344 * will be used. 345 */ registerCallback(@onNull Callback callback, @Nullable Handler handler)346 public void registerCallback(@NonNull Callback callback, @Nullable Handler handler) { 347 if (callback == null) { 348 throw new IllegalArgumentException("callback must not be null"); 349 } 350 if (handler == null) { 351 handler = new Handler(); 352 } 353 synchronized (mLock) { 354 addCallbackLocked(callback, handler); 355 } 356 } 357 358 /** 359 * Unregisters the specified callback. If an update has already been posted 360 * you may still receive it after calling this method. 361 * 362 * @param callback The callback to remove. 363 */ unregisterCallback(@onNull Callback callback)364 public void unregisterCallback(@NonNull Callback callback) { 365 if (callback == null) { 366 throw new IllegalArgumentException("callback must not be null"); 367 } 368 synchronized (mLock) { 369 removeCallbackLocked(callback); 370 } 371 } 372 373 /** 374 * Sends a generic command to the session. It is up to the session creator 375 * to decide what commands and parameters they will support. As such, 376 * commands should only be sent to sessions that the controller owns. 377 * 378 * @param command The command to send 379 * @param args Any parameters to include with the command 380 * @param cb The callback to receive the result on 381 */ sendCommand(@onNull String command, @Nullable Bundle args, @Nullable ResultReceiver cb)382 public void sendCommand(@NonNull String command, @Nullable Bundle args, 383 @Nullable ResultReceiver cb) { 384 if (TextUtils.isEmpty(command)) { 385 throw new IllegalArgumentException("command cannot be null or empty"); 386 } 387 try { 388 mSessionBinder.sendCommand(mContext.getPackageName(), command, args, cb); 389 } catch (RemoteException ex) { 390 throw ex.rethrowFromSystemServer(); 391 } 392 } 393 394 /** 395 * Get the session owner's package name. 396 * 397 * @return The package name of the session owner. 398 */ getPackageName()399 public String getPackageName() { 400 if (mPackageName == null) { 401 try { 402 mPackageName = mSessionBinder.getPackageName(); 403 } catch (RemoteException ex) { 404 throw ex.rethrowFromSystemServer(); 405 } 406 } 407 return mPackageName; 408 } 409 410 /** 411 * Gets the additional session information which was set when the session was created. 412 * 413 * @return The additional session information, or an empty {@link Bundle} if not set. 414 */ 415 @NonNull getSessionInfo()416 public Bundle getSessionInfo() { 417 if (mSessionInfo != null) { 418 return new Bundle(mSessionInfo); 419 } 420 421 // Get info from the connected session. 422 try { 423 mSessionInfo = mSessionBinder.getSessionInfo(); 424 } catch (RemoteException ex) { 425 throw ex.rethrowFromSystemServer(); 426 } 427 428 if (mSessionInfo == null) { 429 Log.d(TAG, "sessionInfo is not set."); 430 mSessionInfo = Bundle.EMPTY; 431 } else if (MediaSession.hasCustomParcelable(mSessionInfo)) { 432 Log.w(TAG, "sessionInfo contains custom parcelable. Ignoring."); 433 mSessionInfo = Bundle.EMPTY; 434 } 435 return new Bundle(mSessionInfo); 436 } 437 438 /** 439 * Get the session's tag for debugging purposes. 440 * 441 * @return The session's tag. 442 */ 443 @NonNull getTag()444 public String getTag() { 445 if (mTag == null) { 446 try { 447 mTag = mSessionBinder.getTag(); 448 } catch (RemoteException ex) { 449 throw ex.rethrowFromSystemServer(); 450 } 451 } 452 return mTag; 453 } 454 455 /** 456 * @hide 457 * Returns whether this and {@code other} media controller controls the same session. 458 */ 459 @UnsupportedAppUsage(publicAlternatives = "Check equality of {@link #getSessionToken() tokens} " 460 + "instead.", maxTargetSdk = Build.VERSION_CODES.R) controlsSameSession(@ullable MediaController other)461 public boolean controlsSameSession(@Nullable MediaController other) { 462 if (other == null) return false; 463 return mToken.equals(other.mToken); 464 } 465 addCallbackLocked(Callback cb, Handler handler)466 private void addCallbackLocked(Callback cb, Handler handler) { 467 if (getHandlerForCallbackLocked(cb) != null) { 468 Log.w(TAG, "Callback is already added, ignoring"); 469 return; 470 } 471 MessageHandler holder = new MessageHandler(handler.getLooper(), cb); 472 mCallbacks.add(holder); 473 holder.mRegistered = true; 474 475 if (!mCbRegistered) { 476 try { 477 mSessionBinder.registerCallback(mContext.getPackageName(), mCbStub); 478 mCbRegistered = true; 479 } catch (RemoteException ex) { 480 throw ex.rethrowFromSystemServer(); 481 } 482 } 483 } 484 removeCallbackLocked(Callback cb)485 private boolean removeCallbackLocked(Callback cb) { 486 boolean success = false; 487 for (int i = mCallbacks.size() - 1; i >= 0; i--) { 488 MessageHandler handler = mCallbacks.get(i); 489 if (cb == handler.mCallback) { 490 mCallbacks.remove(i); 491 success = true; 492 handler.mRegistered = false; 493 } 494 } 495 if (mCbRegistered && mCallbacks.size() == 0) { 496 try { 497 mSessionBinder.unregisterCallback(mCbStub); 498 } catch (RemoteException ex) { 499 throw ex.rethrowFromSystemServer(); 500 } 501 mCbRegistered = false; 502 } 503 return success; 504 } 505 506 /** 507 * Gets associated handler for the given callback. 508 * @hide 509 */ 510 @VisibleForTesting getHandlerForCallback(Callback cb)511 public Handler getHandlerForCallback(Callback cb) { 512 synchronized (mLock) { 513 return getHandlerForCallbackLocked(cb); 514 } 515 } 516 getHandlerForCallbackLocked(Callback cb)517 private MessageHandler getHandlerForCallbackLocked(Callback cb) { 518 if (cb == null) { 519 throw new IllegalArgumentException("Callback cannot be null"); 520 } 521 for (int i = mCallbacks.size() - 1; i >= 0; i--) { 522 MessageHandler handler = mCallbacks.get(i); 523 if (cb == handler.mCallback) { 524 return handler; 525 } 526 } 527 return null; 528 } 529 postMessage(int what, Object obj, Bundle data)530 private void postMessage(int what, Object obj, Bundle data) { 531 synchronized (mLock) { 532 for (int i = mCallbacks.size() - 1; i >= 0; i--) { 533 mCallbacks.get(i).post(what, obj, data); 534 } 535 } 536 } 537 538 /** 539 * Callback for receiving updates from the session. A Callback can be 540 * registered using {@link #registerCallback}. 541 */ 542 public abstract static class Callback { 543 /** 544 * Override to handle the session being destroyed. The session is no 545 * longer valid after this call and calls to it will be ignored. 546 */ onSessionDestroyed()547 public void onSessionDestroyed() { 548 } 549 550 /** 551 * Override to handle custom events sent by the session owner without a 552 * specified interface. Controllers should only handle these for 553 * sessions they own. 554 * 555 * @param event The event from the session. 556 * @param extras Optional parameters for the event, may be null. 557 */ onSessionEvent(@onNull String event, @Nullable Bundle extras)558 public void onSessionEvent(@NonNull String event, @Nullable Bundle extras) { 559 } 560 561 /** 562 * Override to handle changes in playback state. 563 * 564 * @param state The new playback state of the session 565 */ onPlaybackStateChanged(@ullable PlaybackState state)566 public void onPlaybackStateChanged(@Nullable PlaybackState state) { 567 } 568 569 /** 570 * Override to handle changes to the current metadata. 571 * 572 * @param metadata The current metadata for the session or null if none. 573 * @see MediaMetadata 574 */ onMetadataChanged(@ullable MediaMetadata metadata)575 public void onMetadataChanged(@Nullable MediaMetadata metadata) { 576 } 577 578 /** 579 * Override to handle changes to items in the queue. 580 * 581 * @param queue A list of items in the current play queue. It should 582 * include the currently playing item as well as previous and 583 * upcoming items if applicable. 584 * @see MediaSession.QueueItem 585 */ onQueueChanged(@ullable List<MediaSession.QueueItem> queue)586 public void onQueueChanged(@Nullable List<MediaSession.QueueItem> queue) { 587 } 588 589 /** 590 * Override to handle changes to the queue title. 591 * 592 * @param title The title that should be displayed along with the play queue such as 593 * "Now Playing". May be null if there is no such title. 594 */ onQueueTitleChanged(@ullable CharSequence title)595 public void onQueueTitleChanged(@Nullable CharSequence title) { 596 } 597 598 /** 599 * Override to handle changes to the {@link MediaSession} extras. 600 * 601 * @param extras The extras that can include other information associated with the 602 * {@link MediaSession}. 603 */ onExtrasChanged(@ullable Bundle extras)604 public void onExtrasChanged(@Nullable Bundle extras) { 605 } 606 607 /** 608 * Signals a change in the session's {@link PlaybackInfo PlaybackInfo}. 609 * 610 * @param playbackInfo The latest known state of the session's playback info. 611 */ onAudioInfoChanged(@onNull PlaybackInfo playbackInfo)612 public void onAudioInfoChanged(@NonNull PlaybackInfo playbackInfo) {} 613 } 614 615 /** 616 * Interface for controlling media playback on a session. This allows an app 617 * to send media transport commands to the session. 618 */ 619 public final class TransportControls { 620 private static final String TAG = "TransportController"; 621 TransportControls()622 private TransportControls() { 623 } 624 625 /** 626 * Request that the player prepare its playback. In other words, other sessions can continue 627 * to play during the preparation of this session. This method can be used to speed up the 628 * start of the playback. Once the preparation is done, the session will change its playback 629 * state to {@link PlaybackState#STATE_PAUSED}. Afterwards, {@link #play} can be called to 630 * start playback. 631 */ prepare()632 public void prepare() { 633 try { 634 mSessionBinder.prepare(mContext.getPackageName()); 635 } catch (RemoteException ex) { 636 throw ex.rethrowFromSystemServer(); 637 } 638 } 639 640 /** 641 * Request that the player prepare playback for a specific media id. In other words, other 642 * sessions can continue to play during the preparation of this session. This method can be 643 * used to speed up the start of the playback. Once the preparation is done, the session 644 * will change its playback state to {@link PlaybackState#STATE_PAUSED}. Afterwards, 645 * {@link #play} can be called to start playback. If the preparation is not needed, 646 * {@link #playFromMediaId} can be directly called without this method. 647 * 648 * @param mediaId The id of the requested media. 649 * @param extras Optional extras that can include extra information about the media item 650 * to be prepared. 651 */ prepareFromMediaId(String mediaId, Bundle extras)652 public void prepareFromMediaId(String mediaId, Bundle extras) { 653 if (TextUtils.isEmpty(mediaId)) { 654 throw new IllegalArgumentException( 655 "You must specify a non-empty String for prepareFromMediaId."); 656 } 657 try { 658 mSessionBinder.prepareFromMediaId(mContext.getPackageName(), mediaId, extras); 659 } catch (RemoteException ex) { 660 throw ex.rethrowFromSystemServer(); 661 } 662 } 663 664 /** 665 * Request that the player prepare playback for a specific search query. An empty or null 666 * query should be treated as a request to prepare any music. In other words, other sessions 667 * can continue to play during the preparation of this session. This method can be used to 668 * speed up the start of the playback. Once the preparation is done, the session will 669 * change its playback state to {@link PlaybackState#STATE_PAUSED}. Afterwards, 670 * {@link #play} can be called to start playback. If the preparation is not needed, 671 * {@link #playFromSearch} can be directly called without this method. 672 * 673 * @param query The search query. 674 * @param extras Optional extras that can include extra information 675 * about the query. 676 */ prepareFromSearch(String query, Bundle extras)677 public void prepareFromSearch(String query, Bundle extras) { 678 if (query == null) { 679 // This is to remain compatible with 680 // INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH 681 query = ""; 682 } 683 try { 684 mSessionBinder.prepareFromSearch(mContext.getPackageName(), query, extras); 685 } catch (RemoteException ex) { 686 throw ex.rethrowFromSystemServer(); 687 } 688 } 689 690 /** 691 * Request that the player prepare playback for a specific {@link Uri}. In other words, 692 * other sessions can continue to play during the preparation of this session. This method 693 * can be used to speed up the start of the playback. Once the preparation is done, the 694 * session will change its playback state to {@link PlaybackState#STATE_PAUSED}. Afterwards, 695 * {@link #play} can be called to start playback. If the preparation is not needed, 696 * {@link #playFromUri} can be directly called without this method. 697 * 698 * @param uri The URI of the requested media. 699 * @param extras Optional extras that can include extra information about the media item 700 * to be prepared. 701 */ prepareFromUri(Uri uri, Bundle extras)702 public void prepareFromUri(Uri uri, Bundle extras) { 703 if (uri == null || Uri.EMPTY.equals(uri)) { 704 throw new IllegalArgumentException( 705 "You must specify a non-empty Uri for prepareFromUri."); 706 } 707 try { 708 mSessionBinder.prepareFromUri(mContext.getPackageName(), uri, extras); 709 } catch (RemoteException ex) { 710 throw ex.rethrowFromSystemServer(); 711 } 712 } 713 714 /** 715 * Request that the player start its playback at its current position. 716 */ play()717 public void play() { 718 try { 719 mSessionBinder.play(mContext.getPackageName()); 720 } catch (RemoteException ex) { 721 throw ex.rethrowFromSystemServer(); 722 } 723 } 724 725 /** 726 * Request that the player start playback for a specific media id. 727 * 728 * @param mediaId The id of the requested media. 729 * @param extras Optional extras that can include extra information about the media item 730 * to be played. 731 */ playFromMediaId(String mediaId, Bundle extras)732 public void playFromMediaId(String mediaId, Bundle extras) { 733 if (TextUtils.isEmpty(mediaId)) { 734 throw new IllegalArgumentException( 735 "You must specify a non-empty String for playFromMediaId."); 736 } 737 try { 738 mSessionBinder.playFromMediaId(mContext.getPackageName(), mediaId, extras); 739 } catch (RemoteException ex) { 740 throw ex.rethrowFromSystemServer(); 741 } 742 } 743 744 /** 745 * Request that the player start playback for a specific search query. 746 * An empty or null query should be treated as a request to play any 747 * music. 748 * 749 * @param query The search query. 750 * @param extras Optional extras that can include extra information 751 * about the query. 752 */ playFromSearch(String query, Bundle extras)753 public void playFromSearch(String query, Bundle extras) { 754 if (query == null) { 755 // This is to remain compatible with 756 // INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH 757 query = ""; 758 } 759 try { 760 mSessionBinder.playFromSearch(mContext.getPackageName(), query, extras); 761 } catch (RemoteException ex) { 762 throw ex.rethrowFromSystemServer(); 763 } 764 } 765 766 /** 767 * Request that the player start playback for a specific {@link Uri}. 768 * 769 * @param uri The URI of the requested media. 770 * @param extras Optional extras that can include extra information about the media item 771 * to be played. 772 */ playFromUri(Uri uri, Bundle extras)773 public void playFromUri(Uri uri, Bundle extras) { 774 if (uri == null || Uri.EMPTY.equals(uri)) { 775 throw new IllegalArgumentException( 776 "You must specify a non-empty Uri for playFromUri."); 777 } 778 try { 779 mSessionBinder.playFromUri(mContext.getPackageName(), uri, extras); 780 } catch (RemoteException ex) { 781 throw ex.rethrowFromSystemServer(); 782 } 783 } 784 785 /** 786 * Play an item with a specific id in the play queue. If you specify an 787 * id that is not in the play queue, the behavior is undefined. 788 */ skipToQueueItem(long id)789 public void skipToQueueItem(long id) { 790 try { 791 mSessionBinder.skipToQueueItem(mContext.getPackageName(), id); 792 } catch (RemoteException ex) { 793 throw ex.rethrowFromSystemServer(); 794 } 795 } 796 797 /** 798 * Request that the player pause its playback and stay at its current 799 * position. 800 */ pause()801 public void pause() { 802 try { 803 mSessionBinder.pause(mContext.getPackageName()); 804 } catch (RemoteException ex) { 805 throw ex.rethrowFromSystemServer(); 806 } 807 } 808 809 /** 810 * Request that the player stop its playback; it may clear its state in 811 * whatever way is appropriate. 812 */ stop()813 public void stop() { 814 try { 815 mSessionBinder.stop(mContext.getPackageName()); 816 } catch (RemoteException ex) { 817 throw ex.rethrowFromSystemServer(); 818 } 819 } 820 821 /** 822 * Move to a new location in the media stream. 823 * 824 * @param pos Position to move to, in milliseconds. 825 */ seekTo(long pos)826 public void seekTo(long pos) { 827 try { 828 mSessionBinder.seekTo(mContext.getPackageName(), pos); 829 } catch (RemoteException ex) { 830 throw ex.rethrowFromSystemServer(); 831 } 832 } 833 834 /** 835 * Start fast forwarding. If playback is already fast forwarding this 836 * may increase the rate. 837 */ fastForward()838 public void fastForward() { 839 try { 840 mSessionBinder.fastForward(mContext.getPackageName()); 841 } catch (RemoteException ex) { 842 throw ex.rethrowFromSystemServer(); 843 } 844 } 845 846 /** 847 * Skip to the next item. 848 */ skipToNext()849 public void skipToNext() { 850 try { 851 mSessionBinder.next(mContext.getPackageName()); 852 } catch (RemoteException ex) { 853 throw ex.rethrowFromSystemServer(); 854 } 855 } 856 857 /** 858 * Start rewinding. If playback is already rewinding this may increase 859 * the rate. 860 */ rewind()861 public void rewind() { 862 try { 863 mSessionBinder.rewind(mContext.getPackageName()); 864 } catch (RemoteException ex) { 865 throw ex.rethrowFromSystemServer(); 866 } 867 } 868 869 /** 870 * Skip to the previous item. 871 */ skipToPrevious()872 public void skipToPrevious() { 873 try { 874 mSessionBinder.previous(mContext.getPackageName()); 875 } catch (RemoteException ex) { 876 throw ex.rethrowFromSystemServer(); 877 } 878 } 879 880 /** 881 * Rate the current content. This will cause the rating to be set for 882 * the current user. The Rating type must match the type returned by 883 * {@link #getRatingType()}. 884 * 885 * @param rating The rating to set for the current content 886 */ setRating(Rating rating)887 public void setRating(Rating rating) { 888 try { 889 mSessionBinder.rate(mContext.getPackageName(), rating); 890 } catch (RemoteException ex) { 891 throw ex.rethrowFromSystemServer(); 892 } 893 } 894 895 /** 896 * Sets the playback speed. A value of {@code 1.0f} is the default playback value, 897 * and a negative value indicates reverse playback. {@code 0.0f} is not allowed. 898 * 899 * @param speed The playback speed 900 * @throws IllegalArgumentException if the {@code speed} is equal to zero. 901 */ setPlaybackSpeed(float speed)902 public void setPlaybackSpeed(float speed) { 903 if (speed == 0.0f) { 904 throw new IllegalArgumentException("speed must not be zero"); 905 } 906 try { 907 mSessionBinder.setPlaybackSpeed(mContext.getPackageName(), speed); 908 } catch (RemoteException ex) { 909 throw ex.rethrowFromSystemServer(); 910 } 911 } 912 913 /** 914 * Send a custom action back for the {@link MediaSession} to perform. 915 * 916 * @param customAction The action to perform. 917 * @param args Optional arguments to supply to the {@link MediaSession} for this 918 * custom action. 919 */ sendCustomAction(@onNull PlaybackState.CustomAction customAction, @Nullable Bundle args)920 public void sendCustomAction(@NonNull PlaybackState.CustomAction customAction, 921 @Nullable Bundle args) { 922 if (customAction == null) { 923 throw new IllegalArgumentException("CustomAction cannot be null."); 924 } 925 sendCustomAction(customAction.getAction(), args); 926 } 927 928 /** 929 * Send the id and args from a custom action back for the {@link MediaSession} to perform. 930 * 931 * @see #sendCustomAction(PlaybackState.CustomAction action, Bundle args) 932 * @param action The action identifier of the {@link PlaybackState.CustomAction} as 933 * specified by the {@link MediaSession}. 934 * @param args Optional arguments to supply to the {@link MediaSession} for this 935 * custom action. 936 */ sendCustomAction(@onNull String action, @Nullable Bundle args)937 public void sendCustomAction(@NonNull String action, @Nullable Bundle args) { 938 if (TextUtils.isEmpty(action)) { 939 throw new IllegalArgumentException("CustomAction cannot be null."); 940 } 941 try { 942 mSessionBinder.sendCustomAction(mContext.getPackageName(), action, args); 943 } catch (RemoteException ex) { 944 throw ex.rethrowFromSystemServer(); 945 } 946 } 947 } 948 949 /** 950 * Holds information about the current playback and how audio is handled for 951 * this session. 952 */ 953 public static final class PlaybackInfo implements Parcelable { 954 955 /** 956 * @hide 957 */ 958 @IntDef({PLAYBACK_TYPE_LOCAL, PLAYBACK_TYPE_REMOTE}) 959 @Retention(RetentionPolicy.SOURCE) 960 public @interface PlaybackType {} 961 962 /** 963 * The session uses local playback. 964 */ 965 public static final int PLAYBACK_TYPE_LOCAL = 1; 966 /** 967 * The session uses remote playback. 968 */ 969 public static final int PLAYBACK_TYPE_REMOTE = 2; 970 971 private final int mPlaybackType; 972 private final int mVolumeControl; 973 private final int mMaxVolume; 974 private final int mCurrentVolume; 975 private final AudioAttributes mAudioAttrs; 976 private final String mVolumeControlId; 977 978 /** 979 * Creates a new playback info. 980 * 981 * @param playbackType The playback type. Should be {@link #PLAYBACK_TYPE_LOCAL} or {@link 982 * #PLAYBACK_TYPE_REMOTE} 983 * @param volumeControl See {@link #getVolumeControl()}. 984 * @param maxVolume The max volume. Should be equal or greater than zero. 985 * @param currentVolume The current volume. Should be in the interval [0, maxVolume]. 986 * @param audioAttrs The audio attributes for this playback. Should not be null. 987 * @param volumeControlId See {@link #getVolumeControlId()}. 988 * @hide 989 */ 990 @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) PlaybackInfo( @laybackType int playbackType, @ControlType int volumeControl, @IntRange(from = 0) int maxVolume, @IntRange(from = 0) int currentVolume, @NonNull AudioAttributes audioAttrs, @Nullable String volumeControlId)991 public PlaybackInfo( 992 @PlaybackType int playbackType, 993 @ControlType int volumeControl, 994 @IntRange(from = 0) int maxVolume, 995 @IntRange(from = 0) int currentVolume, 996 @NonNull AudioAttributes audioAttrs, 997 @Nullable String volumeControlId) { 998 mPlaybackType = playbackType; 999 mVolumeControl = volumeControl; 1000 mMaxVolume = maxVolume; 1001 mCurrentVolume = currentVolume; 1002 mAudioAttrs = audioAttrs; 1003 mVolumeControlId = volumeControlId; 1004 } 1005 PlaybackInfo(Parcel in)1006 PlaybackInfo(Parcel in) { 1007 mPlaybackType = in.readInt(); 1008 mVolumeControl = in.readInt(); 1009 mMaxVolume = in.readInt(); 1010 mCurrentVolume = in.readInt(); 1011 mAudioAttrs = in.readParcelable(null, android.media.AudioAttributes.class); 1012 mVolumeControlId = in.readString(); 1013 } 1014 1015 /** 1016 * Get the type of playback which affects volume handling. One of: 1017 * <ul> 1018 * <li>{@link #PLAYBACK_TYPE_LOCAL}</li> 1019 * <li>{@link #PLAYBACK_TYPE_REMOTE}</li> 1020 * </ul> 1021 * 1022 * @return The type of playback this session is using. 1023 */ getPlaybackType()1024 public int getPlaybackType() { 1025 return mPlaybackType; 1026 } 1027 1028 /** 1029 * Get the volume control type associated to the session, as indicated by {@link 1030 * VolumeProvider#getVolumeControl()}. 1031 */ getVolumeControl()1032 public int getVolumeControl() { 1033 return mVolumeControl; 1034 } 1035 1036 /** 1037 * Get the maximum volume that may be set for this session. 1038 * 1039 * @return The maximum allowed volume where this session is playing. 1040 */ getMaxVolume()1041 public int getMaxVolume() { 1042 return mMaxVolume; 1043 } 1044 1045 /** 1046 * Get the current volume for this session. 1047 * 1048 * @return The current volume where this session is playing. 1049 */ getCurrentVolume()1050 public int getCurrentVolume() { 1051 return mCurrentVolume; 1052 } 1053 1054 /** 1055 * Get the audio attributes for this session. The attributes will affect volume handling for 1056 * the session. When the playback type is {@link PlaybackInfo#PLAYBACK_TYPE_REMOTE} these 1057 * may be ignored by the remote volume handler. 1058 * 1059 * @return The attributes for this session. 1060 */ getAudioAttributes()1061 public AudioAttributes getAudioAttributes() { 1062 return mAudioAttrs; 1063 } 1064 1065 /** 1066 * Get the routing controller ID for this session, as indicated by {@link 1067 * VolumeProvider#getVolumeControlId()}. Returns null if unset, or if {@link 1068 * #getPlaybackType()} is {@link #PLAYBACK_TYPE_LOCAL}. 1069 */ 1070 @Nullable getVolumeControlId()1071 public String getVolumeControlId() { 1072 return mVolumeControlId; 1073 } 1074 1075 @Override toString()1076 public String toString() { 1077 return "playbackType=" + mPlaybackType + ", volumeControlType=" + mVolumeControl 1078 + ", maxVolume=" + mMaxVolume + ", currentVolume=" + mCurrentVolume 1079 + ", audioAttrs=" + mAudioAttrs + ", volumeControlId=" + mVolumeControlId; 1080 } 1081 1082 @Override describeContents()1083 public int describeContents() { 1084 return 0; 1085 } 1086 1087 @Override writeToParcel(Parcel dest, int flags)1088 public void writeToParcel(Parcel dest, int flags) { 1089 dest.writeInt(mPlaybackType); 1090 dest.writeInt(mVolumeControl); 1091 dest.writeInt(mMaxVolume); 1092 dest.writeInt(mCurrentVolume); 1093 dest.writeParcelable(mAudioAttrs, flags); 1094 dest.writeString(mVolumeControlId); 1095 } 1096 1097 public static final @android.annotation.NonNull Parcelable.Creator<PlaybackInfo> CREATOR = 1098 new Parcelable.Creator<PlaybackInfo>() { 1099 @Override 1100 public PlaybackInfo createFromParcel(Parcel in) { 1101 return new PlaybackInfo(in); 1102 } 1103 1104 @Override 1105 public PlaybackInfo[] newArray(int size) { 1106 return new PlaybackInfo[size]; 1107 } 1108 }; 1109 } 1110 1111 private static final class CallbackStub extends ISessionControllerCallback.Stub { 1112 private final WeakReference<MediaController> mController; 1113 CallbackStub(MediaController controller)1114 CallbackStub(MediaController controller) { 1115 mController = new WeakReference<MediaController>(controller); 1116 } 1117 1118 @Override onSessionDestroyed()1119 public void onSessionDestroyed() { 1120 MediaController controller = mController.get(); 1121 if (controller != null) { 1122 controller.postMessage(MSG_DESTROYED, null, null); 1123 } 1124 } 1125 1126 @Override onEvent(String event, Bundle extras)1127 public void onEvent(String event, Bundle extras) { 1128 MediaController controller = mController.get(); 1129 if (controller != null) { 1130 controller.postMessage(MSG_EVENT, event, extras); 1131 } 1132 } 1133 1134 @Override onPlaybackStateChanged(PlaybackState state)1135 public void onPlaybackStateChanged(PlaybackState state) { 1136 MediaController controller = mController.get(); 1137 if (controller != null) { 1138 controller.postMessage(MSG_UPDATE_PLAYBACK_STATE, state, null); 1139 } 1140 } 1141 1142 @Override onMetadataChanged(MediaMetadata metadata)1143 public void onMetadataChanged(MediaMetadata metadata) { 1144 MediaController controller = mController.get(); 1145 if (controller != null) { 1146 controller.postMessage(MSG_UPDATE_METADATA, metadata, null); 1147 } 1148 } 1149 1150 @Override onQueueChanged(ParceledListSlice queue)1151 public void onQueueChanged(ParceledListSlice queue) { 1152 MediaController controller = mController.get(); 1153 if (controller != null) { 1154 controller.postMessage(MSG_UPDATE_QUEUE, queue, null); 1155 } 1156 } 1157 1158 @Override onQueueTitleChanged(CharSequence title)1159 public void onQueueTitleChanged(CharSequence title) { 1160 MediaController controller = mController.get(); 1161 if (controller != null) { 1162 controller.postMessage(MSG_UPDATE_QUEUE_TITLE, title, null); 1163 } 1164 } 1165 1166 @Override onExtrasChanged(Bundle extras)1167 public void onExtrasChanged(Bundle extras) { 1168 MediaController controller = mController.get(); 1169 if (controller != null) { 1170 controller.postMessage(MSG_UPDATE_EXTRAS, extras, null); 1171 } 1172 } 1173 1174 @Override onVolumeInfoChanged(@onNull PlaybackInfo info)1175 public void onVolumeInfoChanged(@NonNull PlaybackInfo info) { 1176 MediaController controller = mController.get(); 1177 if (controller != null) { 1178 controller.postMessage(MSG_UPDATE_VOLUME, info, null); 1179 } 1180 } 1181 } 1182 1183 private static final class MessageHandler extends Handler { 1184 private final MediaController.Callback mCallback; 1185 private boolean mRegistered = false; 1186 MessageHandler(Looper looper, MediaController.Callback cb)1187 MessageHandler(Looper looper, MediaController.Callback cb) { 1188 super(looper); 1189 mCallback = cb; 1190 } 1191 1192 @Override handleMessage(Message msg)1193 public void handleMessage(Message msg) { 1194 if (!mRegistered) { 1195 return; 1196 } 1197 switch (msg.what) { 1198 case MSG_EVENT: 1199 mCallback.onSessionEvent((String) msg.obj, msg.getData()); 1200 break; 1201 case MSG_UPDATE_PLAYBACK_STATE: 1202 mCallback.onPlaybackStateChanged((PlaybackState) msg.obj); 1203 break; 1204 case MSG_UPDATE_METADATA: 1205 mCallback.onMetadataChanged((MediaMetadata) msg.obj); 1206 break; 1207 case MSG_UPDATE_QUEUE: 1208 mCallback.onQueueChanged(msg.obj == null ? null : 1209 (List<QueueItem>) ((ParceledListSlice) msg.obj).getList()); 1210 break; 1211 case MSG_UPDATE_QUEUE_TITLE: 1212 mCallback.onQueueTitleChanged((CharSequence) msg.obj); 1213 break; 1214 case MSG_UPDATE_EXTRAS: 1215 mCallback.onExtrasChanged((Bundle) msg.obj); 1216 break; 1217 case MSG_UPDATE_VOLUME: 1218 mCallback.onAudioInfoChanged((PlaybackInfo) msg.obj); 1219 break; 1220 case MSG_DESTROYED: 1221 mCallback.onSessionDestroyed(); 1222 break; 1223 } 1224 } 1225 post(int what, Object obj, Bundle data)1226 public void post(int what, Object obj, Bundle data) { 1227 Message msg = obtainMessage(what, obj); 1228 msg.setAsynchronous(true); 1229 msg.setData(data); 1230 msg.sendToTarget(); 1231 } 1232 } 1233 1234 } 1235