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