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(), 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 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 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(), 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 */ 447 @NonNull 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()); 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(), mediaId, extras); 658 } catch (RemoteException e) { 659 Log.wtf(TAG, "Error calling prepare(" + mediaId + ").", e); 660 } 661 } 662 663 /** 664 * Request that the player prepare playback for a specific search query. An empty or null 665 * query should be treated as a request to prepare any music. In other words, other sessions 666 * can continue to play during the preparation of this session. This method can be used to 667 * speed up the start of the playback. Once the preparation is done, the session will 668 * change its playback state to {@link PlaybackState#STATE_PAUSED}. Afterwards, 669 * {@link #play} can be called to start playback. If the preparation is not needed, 670 * {@link #playFromSearch} can be directly called without this method. 671 * 672 * @param query The search query. 673 * @param extras Optional extras that can include extra information 674 * about the query. 675 */ prepareFromSearch(String query, Bundle extras)676 public void prepareFromSearch(String query, Bundle extras) { 677 if (query == null) { 678 // This is to remain compatible with 679 // INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH 680 query = ""; 681 } 682 try { 683 mSessionBinder.prepareFromSearch(mContext.getPackageName(), query, extras); 684 } catch (RemoteException e) { 685 Log.wtf(TAG, "Error calling prepare(" + query + ").", e); 686 } 687 } 688 689 /** 690 * Request that the player prepare playback for a specific {@link Uri}. In other words, 691 * other sessions can continue to play during the preparation of this session. This method 692 * can be used to speed up the start of the playback. Once the preparation is done, the 693 * session will change its playback state to {@link PlaybackState#STATE_PAUSED}. Afterwards, 694 * {@link #play} can be called to start playback. If the preparation is not needed, 695 * {@link #playFromUri} can be directly called without this method. 696 * 697 * @param uri The URI of the requested media. 698 * @param extras Optional extras that can include extra information about the media item 699 * to be prepared. 700 */ prepareFromUri(Uri uri, Bundle extras)701 public void prepareFromUri(Uri uri, Bundle extras) { 702 if (uri == null || Uri.EMPTY.equals(uri)) { 703 throw new IllegalArgumentException( 704 "You must specify a non-empty Uri for prepareFromUri."); 705 } 706 try { 707 mSessionBinder.prepareFromUri(mContext.getPackageName(), uri, extras); 708 } catch (RemoteException e) { 709 Log.wtf(TAG, "Error calling prepare(" + uri + ").", e); 710 } 711 } 712 713 /** 714 * Request that the player start its playback at its current position. 715 */ play()716 public void play() { 717 try { 718 mSessionBinder.play(mContext.getPackageName()); 719 } catch (RemoteException e) { 720 Log.wtf(TAG, "Error calling play.", e); 721 } 722 } 723 724 /** 725 * Request that the player start playback for a specific media id. 726 * 727 * @param mediaId The id of the requested media. 728 * @param extras Optional extras that can include extra information about the media item 729 * to be played. 730 */ playFromMediaId(String mediaId, Bundle extras)731 public void playFromMediaId(String mediaId, Bundle extras) { 732 if (TextUtils.isEmpty(mediaId)) { 733 throw new IllegalArgumentException( 734 "You must specify a non-empty String for playFromMediaId."); 735 } 736 try { 737 mSessionBinder.playFromMediaId(mContext.getPackageName(), mediaId, extras); 738 } catch (RemoteException e) { 739 Log.wtf(TAG, "Error calling play(" + mediaId + ").", e); 740 } 741 } 742 743 /** 744 * Request that the player start playback for a specific search query. 745 * An empty or null query should be treated as a request to play any 746 * music. 747 * 748 * @param query The search query. 749 * @param extras Optional extras that can include extra information 750 * about the query. 751 */ playFromSearch(String query, Bundle extras)752 public void playFromSearch(String query, Bundle extras) { 753 if (query == null) { 754 // This is to remain compatible with 755 // INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH 756 query = ""; 757 } 758 try { 759 mSessionBinder.playFromSearch(mContext.getPackageName(), query, extras); 760 } catch (RemoteException e) { 761 Log.wtf(TAG, "Error calling play(" + query + ").", e); 762 } 763 } 764 765 /** 766 * Request that the player start playback for a specific {@link Uri}. 767 * 768 * @param uri The URI of the requested media. 769 * @param extras Optional extras that can include extra information about the media item 770 * to be played. 771 */ playFromUri(Uri uri, Bundle extras)772 public void playFromUri(Uri uri, Bundle extras) { 773 if (uri == null || Uri.EMPTY.equals(uri)) { 774 throw new IllegalArgumentException( 775 "You must specify a non-empty Uri for playFromUri."); 776 } 777 try { 778 mSessionBinder.playFromUri(mContext.getPackageName(), uri, extras); 779 } catch (RemoteException e) { 780 Log.wtf(TAG, "Error calling play(" + uri + ").", e); 781 } 782 } 783 784 /** 785 * Play an item with a specific id in the play queue. If you specify an 786 * id that is not in the play queue, the behavior is undefined. 787 */ skipToQueueItem(long id)788 public void skipToQueueItem(long id) { 789 try { 790 mSessionBinder.skipToQueueItem(mContext.getPackageName(), id); 791 } catch (RemoteException e) { 792 Log.wtf(TAG, "Error calling skipToItem(" + id + ").", e); 793 } 794 } 795 796 /** 797 * Request that the player pause its playback and stay at its current 798 * position. 799 */ pause()800 public void pause() { 801 try { 802 mSessionBinder.pause(mContext.getPackageName()); 803 } catch (RemoteException e) { 804 Log.wtf(TAG, "Error calling pause.", e); 805 } 806 } 807 808 /** 809 * Request that the player stop its playback; it may clear its state in 810 * whatever way is appropriate. 811 */ stop()812 public void stop() { 813 try { 814 mSessionBinder.stop(mContext.getPackageName()); 815 } catch (RemoteException e) { 816 Log.wtf(TAG, "Error calling stop.", e); 817 } 818 } 819 820 /** 821 * Move to a new location in the media stream. 822 * 823 * @param pos Position to move to, in milliseconds. 824 */ seekTo(long pos)825 public void seekTo(long pos) { 826 try { 827 mSessionBinder.seekTo(mContext.getPackageName(), pos); 828 } catch (RemoteException e) { 829 Log.wtf(TAG, "Error calling seekTo.", e); 830 } 831 } 832 833 /** 834 * Start fast forwarding. If playback is already fast forwarding this 835 * may increase the rate. 836 */ fastForward()837 public void fastForward() { 838 try { 839 mSessionBinder.fastForward(mContext.getPackageName()); 840 } catch (RemoteException e) { 841 Log.wtf(TAG, "Error calling fastForward.", e); 842 } 843 } 844 845 /** 846 * Skip to the next item. 847 */ skipToNext()848 public void skipToNext() { 849 try { 850 mSessionBinder.next(mContext.getPackageName()); 851 } catch (RemoteException e) { 852 Log.wtf(TAG, "Error calling next.", e); 853 } 854 } 855 856 /** 857 * Start rewinding. If playback is already rewinding this may increase 858 * the rate. 859 */ rewind()860 public void rewind() { 861 try { 862 mSessionBinder.rewind(mContext.getPackageName()); 863 } catch (RemoteException e) { 864 Log.wtf(TAG, "Error calling rewind.", e); 865 } 866 } 867 868 /** 869 * Skip to the previous item. 870 */ skipToPrevious()871 public void skipToPrevious() { 872 try { 873 mSessionBinder.previous(mContext.getPackageName()); 874 } catch (RemoteException e) { 875 Log.wtf(TAG, "Error calling previous.", e); 876 } 877 } 878 879 /** 880 * Rate the current content. This will cause the rating to be set for 881 * the current user. The Rating type must match the type returned by 882 * {@link #getRatingType()}. 883 * 884 * @param rating The rating to set for the current content 885 */ setRating(Rating rating)886 public void setRating(Rating rating) { 887 try { 888 mSessionBinder.rate(mContext.getPackageName(), rating); 889 } catch (RemoteException e) { 890 Log.wtf(TAG, "Error calling rate.", e); 891 } 892 } 893 894 /** 895 * Sets the playback speed. A value of {@code 1.0f} is the default playback value, 896 * and a negative value indicates reverse playback. {@code 0.0f} is not allowed. 897 * 898 * @param speed The playback speed 899 * @throws IllegalArgumentException if the {@code speed} is equal to zero. 900 */ setPlaybackSpeed(float speed)901 public void setPlaybackSpeed(float speed) { 902 if (speed == 0.0f) { 903 throw new IllegalArgumentException("speed must not be zero"); 904 } 905 try { 906 mSessionBinder.setPlaybackSpeed(mContext.getPackageName(), speed); 907 } catch (RemoteException e) { 908 Log.wtf(TAG, "Error calling setPlaybackSpeed.", e); 909 } 910 } 911 912 /** 913 * Send a custom action back for the {@link MediaSession} to perform. 914 * 915 * @param customAction The action to perform. 916 * @param args Optional arguments to supply to the {@link MediaSession} for this 917 * custom action. 918 */ sendCustomAction(@onNull PlaybackState.CustomAction customAction, @Nullable Bundle args)919 public void sendCustomAction(@NonNull PlaybackState.CustomAction customAction, 920 @Nullable Bundle args) { 921 if (customAction == null) { 922 throw new IllegalArgumentException("CustomAction cannot be null."); 923 } 924 sendCustomAction(customAction.getAction(), args); 925 } 926 927 /** 928 * Send the id and args from a custom action back for the {@link MediaSession} to perform. 929 * 930 * @see #sendCustomAction(PlaybackState.CustomAction action, Bundle args) 931 * @param action The action identifier of the {@link PlaybackState.CustomAction} as 932 * specified by the {@link MediaSession}. 933 * @param args Optional arguments to supply to the {@link MediaSession} for this 934 * custom action. 935 */ sendCustomAction(@onNull String action, @Nullable Bundle args)936 public void sendCustomAction(@NonNull String action, @Nullable Bundle args) { 937 if (TextUtils.isEmpty(action)) { 938 throw new IllegalArgumentException("CustomAction cannot be null."); 939 } 940 try { 941 mSessionBinder.sendCustomAction(mContext.getPackageName(), action, args); 942 } catch (RemoteException e) { 943 Log.d(TAG, "Dead object in sendCustomAction.", e); 944 } 945 } 946 } 947 948 /** 949 * Holds information about the current playback and how audio is handled for 950 * this session. 951 */ 952 public static final class PlaybackInfo implements Parcelable { 953 /** 954 * The session uses local playback. 955 */ 956 public static final int PLAYBACK_TYPE_LOCAL = 1; 957 /** 958 * The session uses remote playback. 959 */ 960 public static final int PLAYBACK_TYPE_REMOTE = 2; 961 962 private final int mVolumeType; 963 private final int mVolumeControl; 964 private final int mMaxVolume; 965 private final int mCurrentVolume; 966 private final AudioAttributes mAudioAttrs; 967 private final String mVolumeControlId; 968 969 /** 970 * @hide 971 */ PlaybackInfo(int type, int control, int max, int current, AudioAttributes attrs)972 public PlaybackInfo(int type, int control, int max, int current, AudioAttributes attrs) { 973 this(type, control, max, current, attrs, null); 974 } 975 976 /** 977 * @hide 978 */ PlaybackInfo(int type, int control, int max, int current, AudioAttributes attrs, String volumeControlId)979 public PlaybackInfo(int type, int control, int max, int current, AudioAttributes attrs, 980 String volumeControlId) { 981 mVolumeType = type; 982 mVolumeControl = control; 983 mMaxVolume = max; 984 mCurrentVolume = current; 985 mAudioAttrs = attrs; 986 mVolumeControlId = volumeControlId; 987 } 988 PlaybackInfo(Parcel in)989 PlaybackInfo(Parcel in) { 990 mVolumeType = in.readInt(); 991 mVolumeControl = in.readInt(); 992 mMaxVolume = in.readInt(); 993 mCurrentVolume = in.readInt(); 994 mAudioAttrs = in.readParcelable(null); 995 mVolumeControlId = in.readString(); 996 } 997 998 /** 999 * Get the type of playback which affects volume handling. One of: 1000 * <ul> 1001 * <li>{@link #PLAYBACK_TYPE_LOCAL}</li> 1002 * <li>{@link #PLAYBACK_TYPE_REMOTE}</li> 1003 * </ul> 1004 * 1005 * @return The type of playback this session is using. 1006 */ getPlaybackType()1007 public int getPlaybackType() { 1008 return mVolumeType; 1009 } 1010 1011 /** 1012 * Get the type of volume control that can be used. One of: 1013 * <ul> 1014 * <li>{@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}</li> 1015 * <li>{@link VolumeProvider#VOLUME_CONTROL_RELATIVE}</li> 1016 * <li>{@link VolumeProvider#VOLUME_CONTROL_FIXED}</li> 1017 * </ul> 1018 * 1019 * @return The type of volume control that may be used with this 1020 * session. 1021 */ getVolumeControl()1022 public int getVolumeControl() { 1023 return mVolumeControl; 1024 } 1025 1026 /** 1027 * Get the maximum volume that may be set for this session. 1028 * 1029 * @return The maximum allowed volume where this session is playing. 1030 */ getMaxVolume()1031 public int getMaxVolume() { 1032 return mMaxVolume; 1033 } 1034 1035 /** 1036 * Get the current volume for this session. 1037 * 1038 * @return The current volume where this session is playing. 1039 */ getCurrentVolume()1040 public int getCurrentVolume() { 1041 return mCurrentVolume; 1042 } 1043 1044 /** 1045 * Get the audio attributes for this session. The attributes will affect 1046 * volume handling for the session. When the volume type is 1047 * {@link PlaybackInfo#PLAYBACK_TYPE_REMOTE} these may be ignored by the 1048 * remote volume handler. 1049 * 1050 * @return The attributes for this session. 1051 */ getAudioAttributes()1052 public AudioAttributes getAudioAttributes() { 1053 return mAudioAttrs; 1054 } 1055 1056 /** 1057 * Gets the volume control ID for this session. It can be used to identify which 1058 * volume provider is used by the session. 1059 * <p> 1060 * When the session starts to use {@link #PLAYBACK_TYPE_REMOTE remote volume handling}, 1061 * a volume provider should be set and it may set the volume control ID of the provider 1062 * if the session wants to inform which volume provider is used. 1063 * It can be {@code null} if the session didn't set the volume control ID or it uses 1064 * {@link #PLAYBACK_TYPE_LOCAL local playback}. 1065 * </p> 1066 * 1067 * @return the volume control ID for this session or {@code null} if it uses local playback 1068 * or not set. 1069 * @see VolumeProvider#getVolumeControlId() 1070 */ 1071 @Nullable getVolumeControlId()1072 public String getVolumeControlId() { 1073 return mVolumeControlId; 1074 } 1075 1076 @Override toString()1077 public String toString() { 1078 return "volumeType=" + mVolumeType + ", volumeControl=" + mVolumeControl 1079 + ", maxVolume=" + mMaxVolume + ", currentVolume=" + mCurrentVolume 1080 + ", audioAttrs=" + mAudioAttrs + ", volumeControlId=" + mVolumeControlId; 1081 } 1082 1083 @Override describeContents()1084 public int describeContents() { 1085 return 0; 1086 } 1087 1088 @Override writeToParcel(Parcel dest, int flags)1089 public void writeToParcel(Parcel dest, int flags) { 1090 dest.writeInt(mVolumeType); 1091 dest.writeInt(mVolumeControl); 1092 dest.writeInt(mMaxVolume); 1093 dest.writeInt(mCurrentVolume); 1094 dest.writeParcelable(mAudioAttrs, flags); 1095 dest.writeString(mVolumeControlId); 1096 } 1097 1098 public static final @android.annotation.NonNull Parcelable.Creator<PlaybackInfo> CREATOR = 1099 new Parcelable.Creator<PlaybackInfo>() { 1100 @Override 1101 public PlaybackInfo createFromParcel(Parcel in) { 1102 return new PlaybackInfo(in); 1103 } 1104 1105 @Override 1106 public PlaybackInfo[] newArray(int size) { 1107 return new PlaybackInfo[size]; 1108 } 1109 }; 1110 } 1111 1112 private static final class CallbackStub extends ISessionControllerCallback.Stub { 1113 private final WeakReference<MediaController> mController; 1114 CallbackStub(MediaController controller)1115 CallbackStub(MediaController controller) { 1116 mController = new WeakReference<MediaController>(controller); 1117 } 1118 1119 @Override onSessionDestroyed()1120 public void onSessionDestroyed() { 1121 MediaController controller = mController.get(); 1122 if (controller != null) { 1123 controller.postMessage(MSG_DESTROYED, null, null); 1124 } 1125 } 1126 1127 @Override onEvent(String event, Bundle extras)1128 public void onEvent(String event, Bundle extras) { 1129 MediaController controller = mController.get(); 1130 if (controller != null) { 1131 controller.postMessage(MSG_EVENT, event, extras); 1132 } 1133 } 1134 1135 @Override onPlaybackStateChanged(PlaybackState state)1136 public void onPlaybackStateChanged(PlaybackState state) { 1137 MediaController controller = mController.get(); 1138 if (controller != null) { 1139 controller.postMessage(MSG_UPDATE_PLAYBACK_STATE, state, null); 1140 } 1141 } 1142 1143 @Override onMetadataChanged(MediaMetadata metadata)1144 public void onMetadataChanged(MediaMetadata metadata) { 1145 MediaController controller = mController.get(); 1146 if (controller != null) { 1147 controller.postMessage(MSG_UPDATE_METADATA, metadata, null); 1148 } 1149 } 1150 1151 @Override onQueueChanged(ParceledListSlice queue)1152 public void onQueueChanged(ParceledListSlice queue) { 1153 MediaController controller = mController.get(); 1154 if (controller != null) { 1155 controller.postMessage(MSG_UPDATE_QUEUE, queue, null); 1156 } 1157 } 1158 1159 @Override onQueueTitleChanged(CharSequence title)1160 public void onQueueTitleChanged(CharSequence title) { 1161 MediaController controller = mController.get(); 1162 if (controller != null) { 1163 controller.postMessage(MSG_UPDATE_QUEUE_TITLE, title, null); 1164 } 1165 } 1166 1167 @Override onExtrasChanged(Bundle extras)1168 public void onExtrasChanged(Bundle extras) { 1169 MediaController controller = mController.get(); 1170 if (controller != null) { 1171 controller.postMessage(MSG_UPDATE_EXTRAS, extras, null); 1172 } 1173 } 1174 1175 @Override onVolumeInfoChanged(PlaybackInfo info)1176 public void onVolumeInfoChanged(PlaybackInfo info) { 1177 MediaController controller = mController.get(); 1178 if (controller != null) { 1179 controller.postMessage(MSG_UPDATE_VOLUME, info, null); 1180 } 1181 } 1182 } 1183 1184 private static final class MessageHandler extends Handler { 1185 private final MediaController.Callback mCallback; 1186 private boolean mRegistered = false; 1187 MessageHandler(Looper looper, MediaController.Callback cb)1188 MessageHandler(Looper looper, MediaController.Callback cb) { 1189 super(looper); 1190 mCallback = cb; 1191 } 1192 1193 @Override handleMessage(Message msg)1194 public void handleMessage(Message msg) { 1195 if (!mRegistered) { 1196 return; 1197 } 1198 switch (msg.what) { 1199 case MSG_EVENT: 1200 mCallback.onSessionEvent((String) msg.obj, msg.getData()); 1201 break; 1202 case MSG_UPDATE_PLAYBACK_STATE: 1203 mCallback.onPlaybackStateChanged((PlaybackState) msg.obj); 1204 break; 1205 case MSG_UPDATE_METADATA: 1206 mCallback.onMetadataChanged((MediaMetadata) msg.obj); 1207 break; 1208 case MSG_UPDATE_QUEUE: 1209 mCallback.onQueueChanged(msg.obj == null ? null : 1210 (List<QueueItem>) ((ParceledListSlice) msg.obj).getList()); 1211 break; 1212 case MSG_UPDATE_QUEUE_TITLE: 1213 mCallback.onQueueTitleChanged((CharSequence) msg.obj); 1214 break; 1215 case MSG_UPDATE_EXTRAS: 1216 mCallback.onExtrasChanged((Bundle) msg.obj); 1217 break; 1218 case MSG_UPDATE_VOLUME: 1219 mCallback.onAudioInfoChanged((PlaybackInfo) msg.obj); 1220 break; 1221 case MSG_DESTROYED: 1222 mCallback.onSessionDestroyed(); 1223 break; 1224 } 1225 } 1226 post(int what, Object obj, Bundle data)1227 public void post(int what, Object obj, Bundle data) { 1228 Message msg = obtainMessage(what, obj); 1229 msg.setAsynchronous(true); 1230 msg.setData(data); 1231 msg.sendToTarget(); 1232 } 1233 } 1234 1235 } 1236