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