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