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