1 /* 2 * Copyright (C) 2015 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 package android.support.v4.media; 17 18 import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; 19 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_ADD_SUBSCRIPTION; 20 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_CONNECT; 21 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_DISCONNECT; 22 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_GET_MEDIA_ITEM; 23 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_REGISTER_CALLBACK_MESSENGER; 24 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_REMOVE_SUBSCRIPTION; 25 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_SEARCH; 26 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_SEND_CUSTOM_ACTION; 27 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER; 28 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_VERSION_CURRENT; 29 import static android.support.v4.media.MediaBrowserProtocol.DATA_CALLBACK_TOKEN; 30 import static android.support.v4.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION; 31 import static android.support.v4.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION_EXTRAS; 32 import static android.support.v4.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_ID; 33 import static android.support.v4.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_LIST; 34 import static android.support.v4.media.MediaBrowserProtocol.DATA_MEDIA_SESSION_TOKEN; 35 import static android.support.v4.media.MediaBrowserProtocol.DATA_OPTIONS; 36 import static android.support.v4.media.MediaBrowserProtocol.DATA_PACKAGE_NAME; 37 import static android.support.v4.media.MediaBrowserProtocol.DATA_RESULT_RECEIVER; 38 import static android.support.v4.media.MediaBrowserProtocol.DATA_ROOT_HINTS; 39 import static android.support.v4.media.MediaBrowserProtocol.DATA_SEARCH_EXTRAS; 40 import static android.support.v4.media.MediaBrowserProtocol.DATA_SEARCH_QUERY; 41 import static android.support.v4.media.MediaBrowserProtocol.EXTRA_CLIENT_VERSION; 42 import static android.support.v4.media.MediaBrowserProtocol.EXTRA_MESSENGER_BINDER; 43 import static android.support.v4.media.MediaBrowserProtocol.EXTRA_SESSION_BINDER; 44 import static android.support.v4.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT; 45 import static android.support.v4.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT_FAILED; 46 import static android.support.v4.media.MediaBrowserProtocol.SERVICE_MSG_ON_LOAD_CHILDREN; 47 48 import android.content.ComponentName; 49 import android.content.Context; 50 import android.content.Intent; 51 import android.content.ServiceConnection; 52 import android.os.BadParcelableException; 53 import android.os.Binder; 54 import android.os.Build; 55 import android.os.Bundle; 56 import android.os.Handler; 57 import android.os.IBinder; 58 import android.os.Message; 59 import android.os.Messenger; 60 import android.os.Parcel; 61 import android.os.Parcelable; 62 import android.os.RemoteException; 63 import android.support.annotation.IntDef; 64 import android.support.annotation.NonNull; 65 import android.support.annotation.Nullable; 66 import android.support.annotation.RequiresApi; 67 import android.support.annotation.RestrictTo; 68 import android.support.v4.app.BundleCompat; 69 import android.support.v4.media.session.IMediaSession; 70 import android.support.v4.media.session.MediaControllerCompat.TransportControls; 71 import android.support.v4.media.session.MediaSessionCompat; 72 import android.support.v4.os.BuildCompat; 73 import android.support.v4.os.ResultReceiver; 74 import android.support.v4.util.ArrayMap; 75 import android.text.TextUtils; 76 import android.util.Log; 77 78 import java.lang.annotation.Retention; 79 import java.lang.annotation.RetentionPolicy; 80 import java.lang.ref.WeakReference; 81 import java.util.ArrayList; 82 import java.util.Collections; 83 import java.util.List; 84 import java.util.Map; 85 86 /** 87 * Browses media content offered by a {@link MediaBrowserServiceCompat}. 88 * <p> 89 * This object is not thread-safe. All calls should happen on the thread on which the browser 90 * was constructed. 91 * </p><p> 92 * All callback methods will be called from the thread on which the browser was constructed. 93 * </p> 94 * 95 * <div class="special reference"> 96 * <h3>Developer Guides</h3> 97 * <p>For information about building your media application, read the 98 * <a href="{@docRoot}guide/topics/media-apps/index.html">Media Apps</a> developer guide.</p> 99 * </div> 100 */ 101 public final class MediaBrowserCompat { 102 static final String TAG = "MediaBrowserCompat"; 103 static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 104 105 /** 106 * Used as an int extra field to denote the page number to subscribe. 107 * The value of {@code EXTRA_PAGE} should be greater than or equal to 1. 108 * 109 * @see android.service.media.MediaBrowserService.BrowserRoot 110 * @see #EXTRA_PAGE_SIZE 111 */ 112 public static final String EXTRA_PAGE = "android.media.browse.extra.PAGE"; 113 114 /** 115 * Used as an int extra field to denote the number of media items in a page. 116 * The value of {@code EXTRA_PAGE_SIZE} should be greater than or equal to 1. 117 * 118 * @see android.service.media.MediaBrowserService.BrowserRoot 119 * @see #EXTRA_PAGE 120 */ 121 public static final String EXTRA_PAGE_SIZE = "android.media.browse.extra.PAGE_SIZE"; 122 123 /** 124 * Used as a string extra field to denote the target {@link MediaItem}. 125 * 126 * @see #CUSTOM_ACTION_DOWNLOAD 127 * @see #CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE 128 */ 129 public static final String EXTRA_MEDIA_ID = "android.media.browse.extra.MEDIA_ID"; 130 131 /** 132 * Used as a float extra field to denote the current progress during download. The value of this 133 * field must be a float number within [0.0, 1.0]. 134 * 135 * @see #CUSTOM_ACTION_DOWNLOAD 136 * @see CustomActionCallback#onProgressUpdate 137 */ 138 public static final String EXTRA_DOWNLOAD_PROGRESS = 139 "android.media.browse.extra.DOWNLOAD_PROGRESS"; 140 141 /** 142 * Predefined custom action to ask the connected service to download a specific 143 * {@link MediaItem} for offline playback. The id of the media item must be passed in an extra 144 * bundle. The download progress might be delivered to the browser via 145 * {@link CustomActionCallback#onProgressUpdate}. 146 * 147 * @see #EXTRA_MEDIA_ID 148 * @see #EXTRA_DOWNLOAD_PROGRESS 149 * @see #CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE 150 */ 151 public static final String CUSTOM_ACTION_DOWNLOAD = "android.support.v4.media.action.DOWNLOAD"; 152 153 /** 154 * Predefined custom action to ask the connected service to remove the downloaded file of 155 * {@link MediaItem} by the {@link #CUSTOM_ACTION_DOWNLOAD download} action. The id of the 156 * media item must be passed in an extra bundle. 157 * 158 * @see #EXTRA_MEDIA_ID 159 * @see #CUSTOM_ACTION_DOWNLOAD 160 */ 161 public static final String CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE = 162 "android.support.v4.media.action.REMOVE_DOWNLOADED_FILE"; 163 164 private final MediaBrowserImpl mImpl; 165 166 /** 167 * Creates a media browser for the specified media browse service. 168 * 169 * @param context The context. 170 * @param serviceComponent The component name of the media browse service. 171 * @param callback The connection callback. 172 * @param rootHints An optional bundle of service-specific arguments to send 173 * to the media browse service when connecting and retrieving the root id 174 * for browsing, or null if none. The contents of this bundle may affect 175 * the information returned when browsing. 176 * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_RECENT 177 * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_OFFLINE 178 * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_SUGGESTED 179 */ MediaBrowserCompat(Context context, ComponentName serviceComponent, ConnectionCallback callback, Bundle rootHints)180 public MediaBrowserCompat(Context context, ComponentName serviceComponent, 181 ConnectionCallback callback, Bundle rootHints) { 182 // To workaround an issue of {@link #unsubscribe(String, SubscriptionCallback)} on API 24 183 // and 25 devices, use the support library version of implementation on those devices. 184 if (BuildCompat.isAtLeastO()) { 185 mImpl = new MediaBrowserImplApi24(context, serviceComponent, callback, rootHints); 186 } else if (Build.VERSION.SDK_INT >= 23) { 187 mImpl = new MediaBrowserImplApi23(context, serviceComponent, callback, rootHints); 188 } else if (Build.VERSION.SDK_INT >= 21) { 189 mImpl = new MediaBrowserImplApi21(context, serviceComponent, callback, rootHints); 190 } else { 191 mImpl = new MediaBrowserImplBase(context, serviceComponent, callback, rootHints); 192 } 193 } 194 195 /** 196 * Connects to the media browse service. 197 * <p> 198 * The connection callback specified in the constructor will be invoked 199 * when the connection completes or fails. 200 * </p> 201 */ connect()202 public void connect() { 203 mImpl.connect(); 204 } 205 206 /** 207 * Disconnects from the media browse service. 208 * After this, no more callbacks will be received. 209 */ disconnect()210 public void disconnect() { 211 mImpl.disconnect(); 212 } 213 214 /** 215 * Returns whether the browser is connected to the service. 216 */ isConnected()217 public boolean isConnected() { 218 return mImpl.isConnected(); 219 } 220 221 /** 222 * Gets the service component that the media browser is connected to. 223 */ 224 public @NonNull getServiceComponent()225 ComponentName getServiceComponent() { 226 return mImpl.getServiceComponent(); 227 } 228 229 /** 230 * Gets the root id. 231 * <p> 232 * Note that the root id may become invalid or change when when the 233 * browser is disconnected. 234 * </p> 235 * 236 * @throws IllegalStateException if not connected. 237 */ getRoot()238 public @NonNull String getRoot() { 239 return mImpl.getRoot(); 240 } 241 242 /** 243 * Gets any extras for the media service. 244 * 245 * @throws IllegalStateException if not connected. 246 */ 247 public @Nullable getExtras()248 Bundle getExtras() { 249 return mImpl.getExtras(); 250 } 251 252 /** 253 * Gets the media session token associated with the media browser. 254 * <p> 255 * Note that the session token may become invalid or change when when the 256 * browser is disconnected. 257 * </p> 258 * 259 * @return The session token for the browser, never null. 260 * 261 * @throws IllegalStateException if not connected. 262 */ getSessionToken()263 public @NonNull MediaSessionCompat.Token getSessionToken() { 264 return mImpl.getSessionToken(); 265 } 266 267 /** 268 * Queries for information about the media items that are contained within 269 * the specified id and subscribes to receive updates when they change. 270 * <p> 271 * The list of subscriptions is maintained even when not connected and is 272 * restored after the reconnection. It is ok to subscribe while not connected 273 * but the results will not be returned until the connection completes. 274 * </p> 275 * <p> 276 * If the id is already subscribed with a different callback then the new 277 * callback will replace the previous one and the child data will be 278 * reloaded. 279 * </p> 280 * 281 * @param parentId The id of the parent media item whose list of children 282 * will be subscribed. 283 * @param callback The callback to receive the list of children. 284 */ subscribe(@onNull String parentId, @NonNull SubscriptionCallback callback)285 public void subscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) { 286 // Check arguments. 287 if (TextUtils.isEmpty(parentId)) { 288 throw new IllegalArgumentException("parentId is empty"); 289 } 290 if (callback == null) { 291 throw new IllegalArgumentException("callback is null"); 292 } 293 mImpl.subscribe(parentId, null, callback); 294 } 295 296 /** 297 * Queries with service-specific arguments for information about the media items 298 * that are contained within the specified id and subscribes to receive updates 299 * when they change. 300 * <p> 301 * The list of subscriptions is maintained even when not connected and is 302 * restored after the reconnection. It is ok to subscribe while not connected 303 * but the results will not be returned until the connection completes. 304 * </p> 305 * <p> 306 * If the id is already subscribed with a different callback then the new 307 * callback will replace the previous one and the child data will be 308 * reloaded. 309 * </p> 310 * 311 * @param parentId The id of the parent media item whose list of children 312 * will be subscribed. 313 * @param options A bundle of service-specific arguments to send to the media 314 * browse service. The contents of this bundle may affect the 315 * information returned when browsing. 316 * @param callback The callback to receive the list of children. 317 */ subscribe(@onNull String parentId, @NonNull Bundle options, @NonNull SubscriptionCallback callback)318 public void subscribe(@NonNull String parentId, @NonNull Bundle options, 319 @NonNull SubscriptionCallback callback) { 320 // Check arguments. 321 if (TextUtils.isEmpty(parentId)) { 322 throw new IllegalArgumentException("parentId is empty"); 323 } 324 if (callback == null) { 325 throw new IllegalArgumentException("callback is null"); 326 } 327 if (options == null) { 328 throw new IllegalArgumentException("options are null"); 329 } 330 mImpl.subscribe(parentId, options, callback); 331 } 332 333 /** 334 * Unsubscribes for changes to the children of the specified media id. 335 * <p> 336 * The query callback will no longer be invoked for results associated with 337 * this id once this method returns. 338 * </p> 339 * 340 * @param parentId The id of the parent media item whose list of children 341 * will be unsubscribed. 342 */ unsubscribe(@onNull String parentId)343 public void unsubscribe(@NonNull String parentId) { 344 // Check arguments. 345 if (TextUtils.isEmpty(parentId)) { 346 throw new IllegalArgumentException("parentId is empty"); 347 } 348 mImpl.unsubscribe(parentId, null); 349 } 350 351 /** 352 * Unsubscribes for changes to the children of the specified media id. 353 * <p> 354 * The query callback will no longer be invoked for results associated with 355 * this id once this method returns. 356 * </p> 357 * 358 * @param parentId The id of the parent media item whose list of children 359 * will be unsubscribed. 360 * @param callback A callback sent to the media browse service to subscribe. 361 */ unsubscribe(@onNull String parentId, @NonNull SubscriptionCallback callback)362 public void unsubscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) { 363 // Check arguments. 364 if (TextUtils.isEmpty(parentId)) { 365 throw new IllegalArgumentException("parentId is empty"); 366 } 367 if (callback == null) { 368 throw new IllegalArgumentException("callback is null"); 369 } 370 mImpl.unsubscribe(parentId, callback); 371 } 372 373 /** 374 * Retrieves a specific {@link MediaItem} from the connected service. Not 375 * all services may support this, so falling back to subscribing to the 376 * parent's id should be used when unavailable. 377 * 378 * @param mediaId The id of the item to retrieve. 379 * @param cb The callback to receive the result on. 380 */ getItem(final @NonNull String mediaId, @NonNull final ItemCallback cb)381 public void getItem(final @NonNull String mediaId, @NonNull final ItemCallback cb) { 382 mImpl.getItem(mediaId, cb); 383 } 384 385 /** 386 * Searches {@link MediaItem media items} from the connected service. Not all services may 387 * support this, and {@link SearchCallback#onError} will be called if not implemented. 388 * 389 * @param query The search query that contains keywords separated by space. Should not be an 390 * empty string. 391 * @param extras The bundle of service-specific arguments to send to the media browser service. 392 * The contents of this bundle may affect the search result. 393 * @param callback The callback to receive the search result. Must be non-null. 394 * @throws IllegalStateException if the browser is not connected to the media browser service. 395 */ search(@onNull final String query, final Bundle extras, @NonNull SearchCallback callback)396 public void search(@NonNull final String query, final Bundle extras, 397 @NonNull SearchCallback callback) { 398 if (TextUtils.isEmpty(query)) { 399 throw new IllegalArgumentException("query cannot be empty"); 400 } 401 if (callback == null) { 402 throw new IllegalArgumentException("callback cannot be null"); 403 } 404 mImpl.search(query, extras, callback); 405 } 406 407 /** 408 * Sends a custom action to the connected service. If the service doesn't support the given 409 * action, {@link CustomActionCallback#onError} will be called. 410 * 411 * @param action The custom action that will be sent to the connected service. Should not be an 412 * empty string. 413 * @param extras The bundle of service-specific arguments to send to the media browser service. 414 * @param callback The callback to receive the result of the custom action. 415 * @see #CUSTOM_ACTION_DOWNLOAD 416 * @see #CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE 417 */ sendCustomAction(@onNull String action, Bundle extras, @Nullable CustomActionCallback callback)418 public void sendCustomAction(@NonNull String action, Bundle extras, 419 @Nullable CustomActionCallback callback) { 420 if (TextUtils.isEmpty(action)) { 421 throw new IllegalArgumentException("action cannot be empty"); 422 } 423 mImpl.sendCustomAction(action, extras, callback); 424 } 425 426 /** 427 * A class with information on a single media item for use in browsing/searching media. 428 * MediaItems are application dependent so we cannot guarantee that they contain the 429 * right values. 430 */ 431 public static class MediaItem implements Parcelable { 432 private final int mFlags; 433 private final MediaDescriptionCompat mDescription; 434 435 /** @hide */ 436 @RestrictTo(LIBRARY_GROUP) 437 @Retention(RetentionPolicy.SOURCE) 438 @IntDef(flag=true, value = { FLAG_BROWSABLE, FLAG_PLAYABLE }) 439 public @interface Flags { } 440 441 /** 442 * Flag: Indicates that the item has children of its own. 443 */ 444 public static final int FLAG_BROWSABLE = 1 << 0; 445 446 /** 447 * Flag: Indicates that the item is playable. 448 * <p> 449 * The id of this item may be passed to 450 * {@link TransportControls#playFromMediaId(String, Bundle)} 451 * to start playing it. 452 * </p> 453 */ 454 public static final int FLAG_PLAYABLE = 1 << 1; 455 456 /** 457 * Creates an instance from a framework {@link android.media.browse.MediaBrowser.MediaItem} 458 * object. 459 * <p> 460 * This method is only supported on API 21+. On API 20 and below, it returns null. 461 * </p> 462 * 463 * @param itemObj A {@link android.media.browse.MediaBrowser.MediaItem} object. 464 * @return An equivalent {@link MediaItem} object, or null if none. 465 */ fromMediaItem(Object itemObj)466 public static MediaItem fromMediaItem(Object itemObj) { 467 if (itemObj == null || Build.VERSION.SDK_INT < 21) { 468 return null; 469 } 470 int flags = MediaBrowserCompatApi21.MediaItem.getFlags(itemObj); 471 MediaDescriptionCompat description = 472 MediaDescriptionCompat.fromMediaDescription( 473 MediaBrowserCompatApi21.MediaItem.getDescription(itemObj)); 474 return new MediaItem(description, flags); 475 } 476 477 /** 478 * Creates a list of {@link MediaItem} objects from a framework 479 * {@link android.media.browse.MediaBrowser.MediaItem} object list. 480 * <p> 481 * This method is only supported on API 21+. On API 20 and below, it returns null. 482 * </p> 483 * 484 * @param itemList A list of {@link android.media.browse.MediaBrowser.MediaItem} objects. 485 * @return An equivalent list of {@link MediaItem} objects, or null if none. 486 */ fromMediaItemList(List<?> itemList)487 public static List<MediaItem> fromMediaItemList(List<?> itemList) { 488 if (itemList == null || Build.VERSION.SDK_INT < 21) { 489 return null; 490 } 491 List<MediaItem> items = new ArrayList<>(itemList.size()); 492 for (Object itemObj : itemList) { 493 items.add(fromMediaItem(itemObj)); 494 } 495 return items; 496 } 497 498 /** 499 * Create a new MediaItem for use in browsing media. 500 * @param description The description of the media, which must include a 501 * media id. 502 * @param flags The flags for this item. 503 */ MediaItem(@onNull MediaDescriptionCompat description, @Flags int flags)504 public MediaItem(@NonNull MediaDescriptionCompat description, @Flags int flags) { 505 if (description == null) { 506 throw new IllegalArgumentException("description cannot be null"); 507 } 508 if (TextUtils.isEmpty(description.getMediaId())) { 509 throw new IllegalArgumentException("description must have a non-empty media id"); 510 } 511 mFlags = flags; 512 mDescription = description; 513 } 514 515 /** 516 * Private constructor. 517 */ MediaItem(Parcel in)518 MediaItem(Parcel in) { 519 mFlags = in.readInt(); 520 mDescription = MediaDescriptionCompat.CREATOR.createFromParcel(in); 521 } 522 523 @Override describeContents()524 public int describeContents() { 525 return 0; 526 } 527 528 @Override writeToParcel(Parcel out, int flags)529 public void writeToParcel(Parcel out, int flags) { 530 out.writeInt(mFlags); 531 mDescription.writeToParcel(out, flags); 532 } 533 534 @Override toString()535 public String toString() { 536 final StringBuilder sb = new StringBuilder("MediaItem{"); 537 sb.append("mFlags=").append(mFlags); 538 sb.append(", mDescription=").append(mDescription); 539 sb.append('}'); 540 return sb.toString(); 541 } 542 543 public static final Parcelable.Creator<MediaItem> CREATOR = 544 new Parcelable.Creator<MediaItem>() { 545 @Override 546 public MediaItem createFromParcel(Parcel in) { 547 return new MediaItem(in); 548 } 549 550 @Override 551 public MediaItem[] newArray(int size) { 552 return new MediaItem[size]; 553 } 554 }; 555 556 /** 557 * Gets the flags of the item. 558 */ getFlags()559 public @Flags int getFlags() { 560 return mFlags; 561 } 562 563 /** 564 * Returns whether this item is browsable. 565 * @see #FLAG_BROWSABLE 566 */ isBrowsable()567 public boolean isBrowsable() { 568 return (mFlags & FLAG_BROWSABLE) != 0; 569 } 570 571 /** 572 * Returns whether this item is playable. 573 * @see #FLAG_PLAYABLE 574 */ isPlayable()575 public boolean isPlayable() { 576 return (mFlags & FLAG_PLAYABLE) != 0; 577 } 578 579 /** 580 * Returns the description of the media. 581 */ getDescription()582 public @NonNull MediaDescriptionCompat getDescription() { 583 return mDescription; 584 } 585 586 /** 587 * Returns the media id in the {@link MediaDescriptionCompat} for this item. 588 * @see MediaMetadataCompat#METADATA_KEY_MEDIA_ID 589 */ getMediaId()590 public @Nullable String getMediaId() { 591 return mDescription.getMediaId(); 592 } 593 } 594 595 /** 596 * Callbacks for connection related events. 597 */ 598 public static class ConnectionCallback { 599 final Object mConnectionCallbackObj; 600 ConnectionCallbackInternal mConnectionCallbackInternal; 601 ConnectionCallback()602 public ConnectionCallback() { 603 if (Build.VERSION.SDK_INT >= 21) { 604 mConnectionCallbackObj = 605 MediaBrowserCompatApi21.createConnectionCallback(new StubApi21()); 606 } else { 607 mConnectionCallbackObj = null; 608 } 609 } 610 611 /** 612 * Invoked after {@link MediaBrowserCompat#connect()} when the request has successfully 613 * completed. 614 */ onConnected()615 public void onConnected() { 616 } 617 618 /** 619 * Invoked when the client is disconnected from the media browser. 620 */ onConnectionSuspended()621 public void onConnectionSuspended() { 622 } 623 624 /** 625 * Invoked when the connection to the media browser failed. 626 */ onConnectionFailed()627 public void onConnectionFailed() { 628 } 629 setInternalConnectionCallback(ConnectionCallbackInternal connectionCallbackInternal)630 void setInternalConnectionCallback(ConnectionCallbackInternal connectionCallbackInternal) { 631 mConnectionCallbackInternal = connectionCallbackInternal; 632 } 633 634 interface ConnectionCallbackInternal { onConnected()635 void onConnected(); onConnectionSuspended()636 void onConnectionSuspended(); onConnectionFailed()637 void onConnectionFailed(); 638 } 639 640 private class StubApi21 implements MediaBrowserCompatApi21.ConnectionCallback { StubApi21()641 StubApi21() { 642 } 643 644 @Override onConnected()645 public void onConnected() { 646 if (mConnectionCallbackInternal != null) { 647 mConnectionCallbackInternal.onConnected(); 648 } 649 ConnectionCallback.this.onConnected(); 650 } 651 652 @Override onConnectionSuspended()653 public void onConnectionSuspended() { 654 if (mConnectionCallbackInternal != null) { 655 mConnectionCallbackInternal.onConnectionSuspended(); 656 } 657 ConnectionCallback.this.onConnectionSuspended(); 658 } 659 660 @Override onConnectionFailed()661 public void onConnectionFailed() { 662 if (mConnectionCallbackInternal != null) { 663 mConnectionCallbackInternal.onConnectionFailed(); 664 } 665 ConnectionCallback.this.onConnectionFailed(); 666 } 667 } 668 } 669 670 /** 671 * Callbacks for subscription related events. 672 */ 673 public static abstract class SubscriptionCallback { 674 private final Object mSubscriptionCallbackObj; 675 private final IBinder mToken; 676 WeakReference<Subscription> mSubscriptionRef; 677 SubscriptionCallback()678 public SubscriptionCallback() { 679 if (BuildCompat.isAtLeastO()) { 680 mSubscriptionCallbackObj = 681 MediaBrowserCompatApi24.createSubscriptionCallback(new StubApi24()); 682 mToken = null; 683 } else if (Build.VERSION.SDK_INT >= 21) { 684 mSubscriptionCallbackObj = 685 MediaBrowserCompatApi21.createSubscriptionCallback(new StubApi21()); 686 mToken = new Binder(); 687 } else { 688 mSubscriptionCallbackObj = null; 689 mToken = new Binder(); 690 } 691 } 692 693 /** 694 * Called when the list of children is loaded or updated. 695 * 696 * @param parentId The media id of the parent media item. 697 * @param children The children which were loaded. 698 */ onChildrenLoaded(@onNull String parentId, @NonNull List<MediaItem> children)699 public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children) { 700 } 701 702 /** 703 * Called when the list of children is loaded or updated. 704 * 705 * @param parentId The media id of the parent media item. 706 * @param children The children which were loaded. 707 * @param options A bundle of service-specific arguments to send to the media 708 * browse service. The contents of this bundle may affect the 709 * information returned when browsing. 710 */ onChildrenLoaded(@onNull String parentId, @NonNull List<MediaItem> children, @NonNull Bundle options)711 public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children, 712 @NonNull Bundle options) { 713 } 714 715 /** 716 * Called when the id doesn't exist or other errors in subscribing. 717 * <p> 718 * If this is called, the subscription remains until {@link MediaBrowserCompat#unsubscribe} 719 * called, because some errors may heal themselves. 720 * </p> 721 * 722 * @param parentId The media id of the parent media item whose children could not be loaded. 723 */ onError(@onNull String parentId)724 public void onError(@NonNull String parentId) { 725 } 726 727 /** 728 * Called when the id doesn't exist or other errors in subscribing. 729 * <p> 730 * If this is called, the subscription remains until {@link MediaBrowserCompat#unsubscribe} 731 * called, because some errors may heal themselves. 732 * </p> 733 * 734 * @param parentId The media id of the parent media item whose children could 735 * not be loaded. 736 * @param options A bundle of service-specific arguments sent to the media 737 * browse service. 738 */ onError(@onNull String parentId, @NonNull Bundle options)739 public void onError(@NonNull String parentId, @NonNull Bundle options) { 740 } 741 setSubscription(Subscription subscription)742 private void setSubscription(Subscription subscription) { 743 mSubscriptionRef = new WeakReference<>(subscription); 744 } 745 746 private class StubApi21 implements MediaBrowserCompatApi21.SubscriptionCallback { StubApi21()747 StubApi21() { 748 } 749 750 @Override onChildrenLoaded(@onNull String parentId, List<?> children)751 public void onChildrenLoaded(@NonNull String parentId, List<?> children) { 752 Subscription sub = mSubscriptionRef == null ? null : mSubscriptionRef.get(); 753 if (sub == null) { 754 SubscriptionCallback.this.onChildrenLoaded( 755 parentId, MediaItem.fromMediaItemList(children)); 756 } else { 757 List<MediaBrowserCompat.MediaItem> itemList = 758 MediaItem.fromMediaItemList(children); 759 final List<SubscriptionCallback> callbacks = sub.getCallbacks(); 760 final List<Bundle> optionsList = sub.getOptionsList(); 761 for (int i = 0; i < callbacks.size(); ++i) { 762 Bundle options = optionsList.get(i); 763 if (options == null) { 764 SubscriptionCallback.this.onChildrenLoaded(parentId, itemList); 765 } else { 766 SubscriptionCallback.this.onChildrenLoaded( 767 parentId, applyOptions(itemList, options), options); 768 } 769 } 770 } 771 } 772 773 @Override onError(@onNull String parentId)774 public void onError(@NonNull String parentId) { 775 SubscriptionCallback.this.onError(parentId); 776 } 777 applyOptions(List<MediaBrowserCompat.MediaItem> list, final Bundle options)778 List<MediaBrowserCompat.MediaItem> applyOptions(List<MediaBrowserCompat.MediaItem> list, 779 final Bundle options) { 780 if (list == null) { 781 return null; 782 } 783 int page = options.getInt(MediaBrowserCompat.EXTRA_PAGE, -1); 784 int pageSize = options.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, -1); 785 if (page == -1 && pageSize == -1) { 786 return list; 787 } 788 int fromIndex = pageSize * page; 789 int toIndex = fromIndex + pageSize; 790 if (page < 0 || pageSize < 1 || fromIndex >= list.size()) { 791 return Collections.EMPTY_LIST; 792 } 793 if (toIndex > list.size()) { 794 toIndex = list.size(); 795 } 796 return list.subList(fromIndex, toIndex); 797 } 798 799 } 800 801 private class StubApi24 extends StubApi21 802 implements MediaBrowserCompatApi24.SubscriptionCallback { StubApi24()803 StubApi24() { 804 } 805 806 @Override onChildrenLoaded(@onNull String parentId, List<?> children, @NonNull Bundle options)807 public void onChildrenLoaded(@NonNull String parentId, List<?> children, 808 @NonNull Bundle options) { 809 SubscriptionCallback.this.onChildrenLoaded( 810 parentId, MediaItem.fromMediaItemList(children), options); 811 } 812 813 @Override onError(@onNull String parentId, @NonNull Bundle options)814 public void onError(@NonNull String parentId, @NonNull Bundle options) { 815 SubscriptionCallback.this.onError(parentId, options); 816 } 817 } 818 } 819 820 /** 821 * Callback for receiving the result of {@link #getItem}. 822 */ 823 public static abstract class ItemCallback { 824 final Object mItemCallbackObj; 825 ItemCallback()826 public ItemCallback() { 827 if (Build.VERSION.SDK_INT >= 23) { 828 mItemCallbackObj = MediaBrowserCompatApi23.createItemCallback(new StubApi23()); 829 } else { 830 mItemCallbackObj = null; 831 } 832 } 833 834 /** 835 * Called when the item has been returned by the browser service. 836 * 837 * @param item The item that was returned or null if it doesn't exist. 838 */ onItemLoaded(MediaItem item)839 public void onItemLoaded(MediaItem item) { 840 } 841 842 /** 843 * Called when the item doesn't exist or there was an error retrieving it. 844 * 845 * @param itemId The media id of the media item which could not be loaded. 846 */ onError(@onNull String itemId)847 public void onError(@NonNull String itemId) { 848 } 849 850 private class StubApi23 implements MediaBrowserCompatApi23.ItemCallback { StubApi23()851 StubApi23() { 852 } 853 854 @Override onItemLoaded(Parcel itemParcel)855 public void onItemLoaded(Parcel itemParcel) { 856 if (itemParcel == null) { 857 ItemCallback.this.onItemLoaded(null); 858 } else { 859 itemParcel.setDataPosition(0); 860 MediaItem item = 861 MediaBrowserCompat.MediaItem.CREATOR.createFromParcel(itemParcel); 862 itemParcel.recycle(); 863 ItemCallback.this.onItemLoaded(item); 864 } 865 } 866 867 @Override onError(@onNull String itemId)868 public void onError(@NonNull String itemId) { 869 ItemCallback.this.onError(itemId); 870 } 871 } 872 } 873 874 /** 875 * Callback for receiving the result of {@link #search}. 876 */ 877 public abstract static class SearchCallback { 878 /** 879 * Called when the {@link #search} finished successfully. 880 * 881 * @param query The search query sent for the search request to the connected service. 882 * @param extras The bundle of service-specific arguments sent to the connected service. 883 * @param items The list of media items which contains the search result. 884 */ onSearchResult(@onNull String query, Bundle extras, @NonNull List<MediaItem> items)885 public void onSearchResult(@NonNull String query, Bundle extras, 886 @NonNull List<MediaItem> items) { 887 } 888 889 /** 890 * Called when an error happens while {@link #search} or the connected service doesn't 891 * support {@link #search}. 892 * 893 * @param query The search query sent for the search request to the connected service. 894 * @param extras The bundle of service-specific arguments sent to the connected service. 895 */ onError(@onNull String query, Bundle extras)896 public void onError(@NonNull String query, Bundle extras) { 897 } 898 } 899 900 /** 901 * Callback for receiving the result of {@link #sendCustomAction}. 902 */ 903 public abstract static class CustomActionCallback { 904 /** 905 * Called when an interim update was delivered from the connected service while performing 906 * the custom action. 907 * 908 * @param action The custom action sent to the connected service. 909 * @param extras The bundle of service-specific arguments sent to the connected service. 910 * @param data The additional data delivered from the connected service. 911 */ onProgressUpdate(String action, Bundle extras, Bundle data)912 public void onProgressUpdate(String action, Bundle extras, Bundle data) { 913 } 914 915 /** 916 * Called when the custom action finished successfully. 917 * 918 * @param action The custom action sent to the connected service. 919 * @param extras The bundle of service-specific arguments sent to the connected service. 920 * @param resultData The additional data delivered from the connected service. 921 */ onResult(String action, Bundle extras, Bundle resultData)922 public void onResult(String action, Bundle extras, Bundle resultData) { 923 } 924 925 /** 926 * Called when an error happens while performing the custom action or the connected service 927 * doesn't support the requested custom action. 928 * 929 * @param action The custom action sent to the connected service. 930 * @param extras The bundle of service-specific arguments sent to the connected service. 931 * @param data The additional data delivered from the connected service. 932 */ onError(String action, Bundle extras, Bundle data)933 public void onError(String action, Bundle extras, Bundle data) { 934 } 935 } 936 937 interface MediaBrowserImpl { connect()938 void connect(); disconnect()939 void disconnect(); isConnected()940 boolean isConnected(); getServiceComponent()941 ComponentName getServiceComponent(); getRoot()942 @NonNull String getRoot(); getExtras()943 @Nullable Bundle getExtras(); getSessionToken()944 @NonNull MediaSessionCompat.Token getSessionToken(); subscribe(@onNull String parentId, Bundle options, @NonNull SubscriptionCallback callback)945 void subscribe(@NonNull String parentId, Bundle options, 946 @NonNull SubscriptionCallback callback); unsubscribe(@onNull String parentId, SubscriptionCallback callback)947 void unsubscribe(@NonNull String parentId, SubscriptionCallback callback); getItem(final @NonNull String mediaId, @NonNull final ItemCallback cb)948 void getItem(final @NonNull String mediaId, @NonNull final ItemCallback cb); search(@onNull String query, Bundle extras, @NonNull SearchCallback callback)949 void search(@NonNull String query, Bundle extras, @NonNull SearchCallback callback); sendCustomAction(String action, Bundle extras, final CustomActionCallback callback)950 void sendCustomAction(String action, Bundle extras, final CustomActionCallback callback); 951 } 952 953 interface MediaBrowserServiceCallbackImpl { onServiceConnected(Messenger callback, String root, MediaSessionCompat.Token session, Bundle extra)954 void onServiceConnected(Messenger callback, String root, MediaSessionCompat.Token session, 955 Bundle extra); onConnectionFailed(Messenger callback)956 void onConnectionFailed(Messenger callback); onLoadChildren(Messenger callback, String parentId, List list, Bundle options)957 void onLoadChildren(Messenger callback, String parentId, List list, Bundle options); 958 } 959 960 static class MediaBrowserImplBase 961 implements MediaBrowserImpl, MediaBrowserServiceCallbackImpl { 962 static final int CONNECT_STATE_DISCONNECTING = 0; 963 static final int CONNECT_STATE_DISCONNECTED = 1; 964 static final int CONNECT_STATE_CONNECTING = 2; 965 static final int CONNECT_STATE_CONNECTED = 3; 966 static final int CONNECT_STATE_SUSPENDED = 4; 967 968 final Context mContext; 969 final ComponentName mServiceComponent; 970 final ConnectionCallback mCallback; 971 final Bundle mRootHints; 972 final CallbackHandler mHandler = new CallbackHandler(this); 973 private final ArrayMap<String, Subscription> mSubscriptions = new ArrayMap<>(); 974 975 int mState = CONNECT_STATE_DISCONNECTED; 976 MediaServiceConnection mServiceConnection; 977 ServiceBinderWrapper mServiceBinderWrapper; 978 Messenger mCallbacksMessenger; 979 private String mRootId; 980 private MediaSessionCompat.Token mMediaSessionToken; 981 private Bundle mExtras; 982 MediaBrowserImplBase(Context context, ComponentName serviceComponent, ConnectionCallback callback, Bundle rootHints)983 public MediaBrowserImplBase(Context context, ComponentName serviceComponent, 984 ConnectionCallback callback, Bundle rootHints) { 985 if (context == null) { 986 throw new IllegalArgumentException("context must not be null"); 987 } 988 if (serviceComponent == null) { 989 throw new IllegalArgumentException("service component must not be null"); 990 } 991 if (callback == null) { 992 throw new IllegalArgumentException("connection callback must not be null"); 993 } 994 mContext = context; 995 mServiceComponent = serviceComponent; 996 mCallback = callback; 997 mRootHints = rootHints == null ? null : new Bundle(rootHints); 998 } 999 1000 @Override connect()1001 public void connect() { 1002 if (mState != CONNECT_STATE_DISCONNECTING && mState != CONNECT_STATE_DISCONNECTED) { 1003 throw new IllegalStateException("connect() called while neigther disconnecting nor " 1004 + "disconnected (state=" + getStateLabel(mState) + ")"); 1005 } 1006 1007 mState = CONNECT_STATE_CONNECTING; 1008 mHandler.post(new Runnable() { 1009 @Override 1010 public void run() { 1011 // mState could be changed by the Runnable of disconnect() 1012 if (mState == CONNECT_STATE_DISCONNECTING) { 1013 return; 1014 } 1015 mState = CONNECT_STATE_CONNECTING; 1016 // TODO: remove this extra check. 1017 if (DEBUG) { 1018 if (mServiceConnection != null) { 1019 throw new RuntimeException("mServiceConnection should be null. Instead " 1020 + "it is " + mServiceConnection); 1021 } 1022 } 1023 if (mServiceBinderWrapper != null) { 1024 throw new RuntimeException("mServiceBinderWrapper should be null. Instead " 1025 + "it is " + mServiceBinderWrapper); 1026 } 1027 if (mCallbacksMessenger != null) { 1028 throw new RuntimeException("mCallbacksMessenger should be null. Instead " 1029 + "it is " + mCallbacksMessenger); 1030 } 1031 1032 final Intent intent = new Intent(MediaBrowserServiceCompat.SERVICE_INTERFACE); 1033 intent.setComponent(mServiceComponent); 1034 1035 mServiceConnection = new MediaServiceConnection(); 1036 boolean bound = false; 1037 try { 1038 bound = mContext.bindService(intent, mServiceConnection, 1039 Context.BIND_AUTO_CREATE); 1040 } catch (Exception ex) { 1041 Log.e(TAG, "Failed binding to service " + mServiceComponent); 1042 } 1043 1044 if (!bound) { 1045 // Tell them that it didn't work. 1046 forceCloseConnection(); 1047 mCallback.onConnectionFailed(); 1048 } 1049 1050 if (DEBUG) { 1051 Log.d(TAG, "connect..."); 1052 dump(); 1053 } 1054 } 1055 }); 1056 } 1057 1058 @Override disconnect()1059 public void disconnect() { 1060 // It's ok to call this any state, because allowing this lets apps not have 1061 // to check isConnected() unnecessarily. They won't appreciate the extra 1062 // assertions for this. We do everything we can here to go back to a sane state. 1063 mState = CONNECT_STATE_DISCONNECTING; 1064 mHandler.post(new Runnable() { 1065 @Override 1066 public void run() { 1067 // connect() could be called before this. Then we will disconnect and reconnect. 1068 if (mCallbacksMessenger != null) { 1069 try { 1070 mServiceBinderWrapper.disconnect(mCallbacksMessenger); 1071 } catch (RemoteException ex) { 1072 // We are disconnecting anyway. Log, just for posterity but it's not 1073 // a big problem. 1074 Log.w(TAG, "RemoteException during connect for " + mServiceComponent); 1075 } 1076 } 1077 int state = mState; 1078 forceCloseConnection(); 1079 // If the state was not CONNECT_STATE_DISCONNECTING, keep the state so that 1080 // the operation came after disconnect() can be handled properly. 1081 if (state != CONNECT_STATE_DISCONNECTING) { 1082 mState = state; 1083 } 1084 if (DEBUG) { 1085 Log.d(TAG, "disconnect..."); 1086 dump(); 1087 } 1088 } 1089 }); 1090 } 1091 1092 /** 1093 * Null out the variables and unbind from the service. This doesn't include 1094 * calling disconnect on the service, because we only try to do that in the 1095 * clean shutdown cases. 1096 * <p> 1097 * Everywhere that calls this EXCEPT for disconnect() should follow it with 1098 * a call to mCallback.onConnectionFailed(). Disconnect doesn't do that callback 1099 * for a clean shutdown, but everywhere else is a dirty shutdown and should 1100 * notify the app. 1101 */ forceCloseConnection()1102 void forceCloseConnection() { 1103 if (mServiceConnection != null) { 1104 mContext.unbindService(mServiceConnection); 1105 } 1106 mState = CONNECT_STATE_DISCONNECTED; 1107 mServiceConnection = null; 1108 mServiceBinderWrapper = null; 1109 mCallbacksMessenger = null; 1110 mHandler.setCallbacksMessenger(null); 1111 mRootId = null; 1112 mMediaSessionToken = null; 1113 } 1114 1115 @Override isConnected()1116 public boolean isConnected() { 1117 return mState == CONNECT_STATE_CONNECTED; 1118 } 1119 1120 @Override getServiceComponent()1121 public @NonNull ComponentName getServiceComponent() { 1122 if (!isConnected()) { 1123 throw new IllegalStateException("getServiceComponent() called while not connected" + 1124 " (state=" + mState + ")"); 1125 } 1126 return mServiceComponent; 1127 } 1128 1129 @Override getRoot()1130 public @NonNull String getRoot() { 1131 if (!isConnected()) { 1132 throw new IllegalStateException("getRoot() called while not connected" 1133 + "(state=" + getStateLabel(mState) + ")"); 1134 } 1135 return mRootId; 1136 } 1137 1138 @Override getExtras()1139 public @Nullable Bundle getExtras() { 1140 if (!isConnected()) { 1141 throw new IllegalStateException("getExtras() called while not connected (state=" 1142 + getStateLabel(mState) + ")"); 1143 } 1144 return mExtras; 1145 } 1146 1147 @Override getSessionToken()1148 public @NonNull MediaSessionCompat.Token getSessionToken() { 1149 if (!isConnected()) { 1150 throw new IllegalStateException("getSessionToken() called while not connected" 1151 + "(state=" + mState + ")"); 1152 } 1153 return mMediaSessionToken; 1154 } 1155 1156 @Override subscribe(@onNull String parentId, Bundle options, @NonNull SubscriptionCallback callback)1157 public void subscribe(@NonNull String parentId, Bundle options, 1158 @NonNull SubscriptionCallback callback) { 1159 // Update or create the subscription. 1160 Subscription sub = mSubscriptions.get(parentId); 1161 if (sub == null) { 1162 sub = new Subscription(); 1163 mSubscriptions.put(parentId, sub); 1164 } 1165 Bundle copiedOptions = options == null ? null : new Bundle(options); 1166 sub.putCallback(copiedOptions, callback); 1167 1168 // If we are connected, tell the service that we are watching. If we aren't 1169 // connected, the service will be told when we connect. 1170 if (isConnected()) { 1171 try { 1172 mServiceBinderWrapper.addSubscription(parentId, callback.mToken, copiedOptions, 1173 mCallbacksMessenger); 1174 } catch (RemoteException e) { 1175 // Process is crashing. We will disconnect, and upon reconnect we will 1176 // automatically reregister. So nothing to do here. 1177 Log.d(TAG, "addSubscription failed with RemoteException parentId=" + parentId); 1178 } 1179 } 1180 } 1181 1182 @Override unsubscribe(@onNull String parentId, SubscriptionCallback callback)1183 public void unsubscribe(@NonNull String parentId, SubscriptionCallback callback) { 1184 Subscription sub = mSubscriptions.get(parentId); 1185 if (sub == null) { 1186 return; 1187 } 1188 1189 // Tell the service if necessary. 1190 try { 1191 if (callback == null) { 1192 if (isConnected()) { 1193 mServiceBinderWrapper.removeSubscription(parentId, null, 1194 mCallbacksMessenger); 1195 } 1196 } else { 1197 final List<SubscriptionCallback> callbacks = sub.getCallbacks(); 1198 final List<Bundle> optionsList = sub.getOptionsList(); 1199 for (int i = callbacks.size() - 1; i >= 0; --i) { 1200 if (callbacks.get(i) == callback) { 1201 if (isConnected()) { 1202 mServiceBinderWrapper.removeSubscription( 1203 parentId, callback.mToken, mCallbacksMessenger); 1204 } 1205 callbacks.remove(i); 1206 optionsList.remove(i); 1207 } 1208 } 1209 } 1210 } catch (RemoteException ex) { 1211 // Process is crashing. We will disconnect, and upon reconnect we will 1212 // automatically reregister. So nothing to do here. 1213 Log.d(TAG, "removeSubscription failed with RemoteException parentId=" + parentId); 1214 } 1215 1216 if (sub.isEmpty() || callback == null) { 1217 mSubscriptions.remove(parentId); 1218 } 1219 } 1220 1221 @Override getItem(@onNull final String mediaId, @NonNull final ItemCallback cb)1222 public void getItem(@NonNull final String mediaId, @NonNull final ItemCallback cb) { 1223 if (TextUtils.isEmpty(mediaId)) { 1224 throw new IllegalArgumentException("mediaId is empty"); 1225 } 1226 if (cb == null) { 1227 throw new IllegalArgumentException("cb is null"); 1228 } 1229 if (!isConnected()) { 1230 Log.i(TAG, "Not connected, unable to retrieve the MediaItem."); 1231 mHandler.post(new Runnable() { 1232 @Override 1233 public void run() { 1234 cb.onError(mediaId); 1235 } 1236 }); 1237 return; 1238 } 1239 ResultReceiver receiver = new ItemReceiver(mediaId, cb, mHandler); 1240 try { 1241 mServiceBinderWrapper.getMediaItem(mediaId, receiver, mCallbacksMessenger); 1242 } catch (RemoteException e) { 1243 Log.i(TAG, "Remote error getting media item: " + mediaId); 1244 mHandler.post(new Runnable() { 1245 @Override 1246 public void run() { 1247 cb.onError(mediaId); 1248 } 1249 }); 1250 } 1251 } 1252 1253 @Override search(@onNull final String query, final Bundle extras, @NonNull final SearchCallback callback)1254 public void search(@NonNull final String query, final Bundle extras, 1255 @NonNull final SearchCallback callback) { 1256 if (!isConnected()) { 1257 throw new IllegalStateException("search() called while not connected" 1258 + " (state=" + getStateLabel(mState) + ")"); 1259 } 1260 1261 ResultReceiver receiver = new SearchResultReceiver(query, extras, callback, mHandler); 1262 try { 1263 mServiceBinderWrapper.search(query, extras, receiver, mCallbacksMessenger); 1264 } catch (RemoteException e) { 1265 Log.i(TAG, "Remote error searching items with query: " + query, e); 1266 mHandler.post(new Runnable() { 1267 @Override 1268 public void run() { 1269 callback.onError(query, extras); 1270 } 1271 }); 1272 } 1273 } 1274 1275 @Override sendCustomAction(@onNull final String action, final Bundle extras, @Nullable final CustomActionCallback callback)1276 public void sendCustomAction(@NonNull final String action, final Bundle extras, 1277 @Nullable final CustomActionCallback callback) { 1278 if (!isConnected()) { 1279 throw new IllegalStateException("Cannot send a custom action (" + action + ") with " 1280 + "extras " + extras + " because the browser is not connected to the " 1281 + "service."); 1282 } 1283 1284 ResultReceiver receiver = new CustomActionResultReceiver(action, extras, callback, 1285 mHandler); 1286 try { 1287 mServiceBinderWrapper.sendCustomAction(action, extras, receiver, 1288 mCallbacksMessenger); 1289 } catch (RemoteException e) { 1290 Log.i(TAG, "Remote error sending a custom action: action=" + action + ", extras=" 1291 + extras, e); 1292 mHandler.post(new Runnable() { 1293 @Override 1294 public void run() { 1295 callback.onError(action, extras, null); 1296 } 1297 }); 1298 } 1299 } 1300 1301 @Override onServiceConnected(final Messenger callback, final String root, final MediaSessionCompat.Token session, final Bundle extra)1302 public void onServiceConnected(final Messenger callback, final String root, 1303 final MediaSessionCompat.Token session, final Bundle extra) { 1304 // Check to make sure there hasn't been a disconnect or a different ServiceConnection. 1305 if (!isCurrent(callback, "onConnect")) { 1306 return; 1307 } 1308 // Don't allow them to call us twice. 1309 if (mState != CONNECT_STATE_CONNECTING) { 1310 Log.w(TAG, "onConnect from service while mState=" + getStateLabel(mState) 1311 + "... ignoring"); 1312 return; 1313 } 1314 mRootId = root; 1315 mMediaSessionToken = session; 1316 mExtras = extra; 1317 mState = CONNECT_STATE_CONNECTED; 1318 1319 if (DEBUG) { 1320 Log.d(TAG, "ServiceCallbacks.onConnect..."); 1321 dump(); 1322 } 1323 mCallback.onConnected(); 1324 1325 // we may receive some subscriptions before we are connected, so re-subscribe 1326 // everything now 1327 try { 1328 for (Map.Entry<String, Subscription> subscriptionEntry 1329 : mSubscriptions.entrySet()) { 1330 String id = subscriptionEntry.getKey(); 1331 Subscription sub = subscriptionEntry.getValue(); 1332 List<SubscriptionCallback> callbackList = sub.getCallbacks(); 1333 List<Bundle> optionsList = sub.getOptionsList(); 1334 for (int i = 0; i < callbackList.size(); ++i) { 1335 mServiceBinderWrapper.addSubscription(id, callbackList.get(i).mToken, 1336 optionsList.get(i), mCallbacksMessenger); 1337 } 1338 } 1339 } catch (RemoteException ex) { 1340 // Process is crashing. We will disconnect, and upon reconnect we will 1341 // automatically reregister. So nothing to do here. 1342 Log.d(TAG, "addSubscription failed with RemoteException."); 1343 } 1344 } 1345 1346 @Override onConnectionFailed(final Messenger callback)1347 public void onConnectionFailed(final Messenger callback) { 1348 Log.e(TAG, "onConnectFailed for " + mServiceComponent); 1349 1350 // Check to make sure there hasn't been a disconnect or a different ServiceConnection. 1351 if (!isCurrent(callback, "onConnectFailed")) { 1352 return; 1353 } 1354 // Don't allow them to call us twice. 1355 if (mState != CONNECT_STATE_CONNECTING) { 1356 Log.w(TAG, "onConnect from service while mState=" + getStateLabel(mState) 1357 + "... ignoring"); 1358 return; 1359 } 1360 1361 // Clean up 1362 forceCloseConnection(); 1363 1364 // Tell the app. 1365 mCallback.onConnectionFailed(); 1366 } 1367 1368 @Override onLoadChildren(final Messenger callback, final String parentId, final List list, final Bundle options)1369 public void onLoadChildren(final Messenger callback, final String parentId, 1370 final List list, final Bundle options) { 1371 // Check that there hasn't been a disconnect or a different ServiceConnection. 1372 if (!isCurrent(callback, "onLoadChildren")) { 1373 return; 1374 } 1375 1376 if (DEBUG) { 1377 Log.d(TAG, "onLoadChildren for " + mServiceComponent + " id=" + parentId); 1378 } 1379 1380 // Check that the subscription is still subscribed. 1381 final Subscription subscription = mSubscriptions.get(parentId); 1382 if (subscription == null) { 1383 if (DEBUG) { 1384 Log.d(TAG, "onLoadChildren for id that isn't subscribed id=" + parentId); 1385 } 1386 return; 1387 } 1388 1389 // Tell the app. 1390 SubscriptionCallback subscriptionCallback = subscription.getCallback(options); 1391 if (subscriptionCallback != null) { 1392 if (options == null) { 1393 if (list == null) { 1394 subscriptionCallback.onError(parentId); 1395 } else { 1396 subscriptionCallback.onChildrenLoaded(parentId, list); 1397 } 1398 } else { 1399 if (list == null) { 1400 subscriptionCallback.onError(parentId, options); 1401 } else { 1402 subscriptionCallback.onChildrenLoaded(parentId, list, options); 1403 } 1404 } 1405 } 1406 } 1407 1408 /** 1409 * For debugging. 1410 */ getStateLabel(int state)1411 private static String getStateLabel(int state) { 1412 switch (state) { 1413 case CONNECT_STATE_DISCONNECTING: 1414 return "CONNECT_STATE_DISCONNECTING"; 1415 case CONNECT_STATE_DISCONNECTED: 1416 return "CONNECT_STATE_DISCONNECTED"; 1417 case CONNECT_STATE_CONNECTING: 1418 return "CONNECT_STATE_CONNECTING"; 1419 case CONNECT_STATE_CONNECTED: 1420 return "CONNECT_STATE_CONNECTED"; 1421 case CONNECT_STATE_SUSPENDED: 1422 return "CONNECT_STATE_SUSPENDED"; 1423 default: 1424 return "UNKNOWN/" + state; 1425 } 1426 } 1427 1428 /** 1429 * Return true if {@code callback} is the current ServiceCallbacks. Also logs if it's not. 1430 */ 1431 @SuppressWarnings("ReferenceEquality") isCurrent(Messenger callback, String funcName)1432 private boolean isCurrent(Messenger callback, String funcName) { 1433 if (mCallbacksMessenger != callback || mState == CONNECT_STATE_DISCONNECTING 1434 || mState == CONNECT_STATE_DISCONNECTED) { 1435 if (mState != CONNECT_STATE_DISCONNECTING && mState != CONNECT_STATE_DISCONNECTED) { 1436 Log.i(TAG, funcName + " for " + mServiceComponent + " with mCallbacksMessenger=" 1437 + mCallbacksMessenger + " this=" + this); 1438 } 1439 return false; 1440 } 1441 return true; 1442 } 1443 1444 /** 1445 * Log internal state. 1446 */ dump()1447 void dump() { 1448 Log.d(TAG, "MediaBrowserCompat..."); 1449 Log.d(TAG, " mServiceComponent=" + mServiceComponent); 1450 Log.d(TAG, " mCallback=" + mCallback); 1451 Log.d(TAG, " mRootHints=" + mRootHints); 1452 Log.d(TAG, " mState=" + getStateLabel(mState)); 1453 Log.d(TAG, " mServiceConnection=" + mServiceConnection); 1454 Log.d(TAG, " mServiceBinderWrapper=" + mServiceBinderWrapper); 1455 Log.d(TAG, " mCallbacksMessenger=" + mCallbacksMessenger); 1456 Log.d(TAG, " mRootId=" + mRootId); 1457 Log.d(TAG, " mMediaSessionToken=" + mMediaSessionToken); 1458 } 1459 1460 /** 1461 * ServiceConnection to the other app. 1462 */ 1463 private class MediaServiceConnection implements ServiceConnection { MediaServiceConnection()1464 MediaServiceConnection() { 1465 } 1466 1467 @Override onServiceConnected(final ComponentName name, final IBinder binder)1468 public void onServiceConnected(final ComponentName name, final IBinder binder) { 1469 postOrRun(new Runnable() { 1470 @Override 1471 public void run() { 1472 if (DEBUG) { 1473 Log.d(TAG, "MediaServiceConnection.onServiceConnected name=" + name 1474 + " binder=" + binder); 1475 dump(); 1476 } 1477 1478 // Make sure we are still the current connection, and that they haven't 1479 // called disconnect(). 1480 if (!isCurrent("onServiceConnected")) { 1481 return; 1482 } 1483 1484 // Save their binder 1485 mServiceBinderWrapper = new ServiceBinderWrapper(binder, mRootHints); 1486 1487 // We make a new mServiceCallbacks each time we connect so that we can drop 1488 // responses from previous connections. 1489 mCallbacksMessenger = new Messenger(mHandler); 1490 mHandler.setCallbacksMessenger(mCallbacksMessenger); 1491 1492 mState = CONNECT_STATE_CONNECTING; 1493 1494 // Call connect, which is async. When we get a response from that we will 1495 // say that we're connected. 1496 try { 1497 if (DEBUG) { 1498 Log.d(TAG, "ServiceCallbacks.onConnect..."); 1499 dump(); 1500 } 1501 mServiceBinderWrapper.connect(mContext, mCallbacksMessenger); 1502 } catch (RemoteException ex) { 1503 // Connect failed, which isn't good. But the auto-reconnect on the 1504 // service will take over and we will come back. We will also get the 1505 // onServiceDisconnected, which has all the cleanup code. So let that 1506 // do it. 1507 Log.w(TAG, "RemoteException during connect for " + mServiceComponent); 1508 if (DEBUG) { 1509 Log.d(TAG, "ServiceCallbacks.onConnect..."); 1510 dump(); 1511 } 1512 } 1513 } 1514 }); 1515 } 1516 1517 @Override onServiceDisconnected(final ComponentName name)1518 public void onServiceDisconnected(final ComponentName name) { 1519 postOrRun(new Runnable() { 1520 @Override 1521 public void run() { 1522 if (DEBUG) { 1523 Log.d(TAG, "MediaServiceConnection.onServiceDisconnected name=" + name 1524 + " this=" + this + " mServiceConnection=" + 1525 mServiceConnection); 1526 dump(); 1527 } 1528 1529 // Make sure we are still the current connection, and that they haven't 1530 // called disconnect(). 1531 if (!isCurrent("onServiceDisconnected")) { 1532 return; 1533 } 1534 1535 // Clear out what we set in onServiceConnected 1536 mServiceBinderWrapper = null; 1537 mCallbacksMessenger = null; 1538 mHandler.setCallbacksMessenger(null); 1539 1540 // And tell the app that it's suspended. 1541 mState = CONNECT_STATE_SUSPENDED; 1542 mCallback.onConnectionSuspended(); 1543 } 1544 }); 1545 } 1546 postOrRun(Runnable r)1547 private void postOrRun(Runnable r) { 1548 if (Thread.currentThread() == mHandler.getLooper().getThread()) { 1549 r.run(); 1550 } else { 1551 mHandler.post(r); 1552 } 1553 } 1554 1555 /** 1556 * Return true if this is the current ServiceConnection. Also logs if it's not. 1557 */ isCurrent(String funcName)1558 boolean isCurrent(String funcName) { 1559 if (mServiceConnection != this || mState == CONNECT_STATE_DISCONNECTING 1560 || mState == CONNECT_STATE_DISCONNECTED) { 1561 if (mState != CONNECT_STATE_DISCONNECTING 1562 && mState != CONNECT_STATE_DISCONNECTED) { 1563 // Check mState, because otherwise this log is noisy. 1564 Log.i(TAG, funcName + " for " + mServiceComponent + 1565 " with mServiceConnection=" + mServiceConnection + " this=" + this); 1566 } 1567 return false; 1568 } 1569 return true; 1570 } 1571 } 1572 } 1573 1574 @RequiresApi(21) 1575 static class MediaBrowserImplApi21 implements MediaBrowserImpl, MediaBrowserServiceCallbackImpl, 1576 ConnectionCallback.ConnectionCallbackInternal { 1577 protected final Object mBrowserObj; 1578 protected final Bundle mRootHints; 1579 protected final CallbackHandler mHandler = new CallbackHandler(this); 1580 private final ArrayMap<String, Subscription> mSubscriptions = new ArrayMap<>(); 1581 1582 protected ServiceBinderWrapper mServiceBinderWrapper; 1583 protected Messenger mCallbacksMessenger; 1584 private MediaSessionCompat.Token mMediaSessionToken; 1585 MediaBrowserImplApi21(Context context, ComponentName serviceComponent, ConnectionCallback callback, Bundle rootHints)1586 public MediaBrowserImplApi21(Context context, ComponentName serviceComponent, 1587 ConnectionCallback callback, Bundle rootHints) { 1588 if (rootHints == null) { 1589 rootHints = new Bundle(); 1590 } 1591 rootHints.putInt(EXTRA_CLIENT_VERSION, CLIENT_VERSION_CURRENT); 1592 mRootHints = new Bundle(rootHints); 1593 callback.setInternalConnectionCallback(this); 1594 mBrowserObj = MediaBrowserCompatApi21.createBrowser(context, serviceComponent, 1595 callback.mConnectionCallbackObj, mRootHints); 1596 } 1597 1598 @Override connect()1599 public void connect() { 1600 MediaBrowserCompatApi21.connect(mBrowserObj); 1601 } 1602 1603 @Override disconnect()1604 public void disconnect() { 1605 if (mServiceBinderWrapper != null && mCallbacksMessenger != null) { 1606 try { 1607 mServiceBinderWrapper.unregisterCallbackMessenger(mCallbacksMessenger); 1608 } catch (RemoteException e) { 1609 Log.i(TAG, "Remote error unregistering client messenger." ); 1610 } 1611 } 1612 MediaBrowserCompatApi21.disconnect(mBrowserObj); 1613 } 1614 1615 @Override isConnected()1616 public boolean isConnected() { 1617 return MediaBrowserCompatApi21.isConnected(mBrowserObj); 1618 } 1619 1620 @Override getServiceComponent()1621 public ComponentName getServiceComponent() { 1622 return MediaBrowserCompatApi21.getServiceComponent(mBrowserObj); 1623 } 1624 1625 @NonNull 1626 @Override getRoot()1627 public String getRoot() { 1628 return MediaBrowserCompatApi21.getRoot(mBrowserObj); 1629 } 1630 1631 @Nullable 1632 @Override getExtras()1633 public Bundle getExtras() { 1634 return MediaBrowserCompatApi21.getExtras(mBrowserObj); 1635 } 1636 1637 @NonNull 1638 @Override getSessionToken()1639 public MediaSessionCompat.Token getSessionToken() { 1640 if (mMediaSessionToken == null) { 1641 mMediaSessionToken = MediaSessionCompat.Token.fromToken( 1642 MediaBrowserCompatApi21.getSessionToken(mBrowserObj)); 1643 } 1644 return mMediaSessionToken; 1645 } 1646 1647 @Override subscribe(@onNull final String parentId, final Bundle options, @NonNull final SubscriptionCallback callback)1648 public void subscribe(@NonNull final String parentId, final Bundle options, 1649 @NonNull final SubscriptionCallback callback) { 1650 // Update or create the subscription. 1651 Subscription sub = mSubscriptions.get(parentId); 1652 if (sub == null) { 1653 sub = new Subscription(); 1654 mSubscriptions.put(parentId, sub); 1655 } 1656 callback.setSubscription(sub); 1657 Bundle copiedOptions = options == null ? null : new Bundle(options); 1658 sub.putCallback(copiedOptions, callback); 1659 1660 if (mServiceBinderWrapper == null) { 1661 // TODO: When MediaBrowser is connected to framework's MediaBrowserService, 1662 // subscribe with options won't work properly. 1663 MediaBrowserCompatApi21.subscribe( 1664 mBrowserObj, parentId, callback.mSubscriptionCallbackObj); 1665 } else { 1666 try { 1667 mServiceBinderWrapper.addSubscription( 1668 parentId, callback.mToken, copiedOptions, mCallbacksMessenger); 1669 } catch (RemoteException e) { 1670 // Process is crashing. We will disconnect, and upon reconnect we will 1671 // automatically reregister. So nothing to do here. 1672 Log.i(TAG, "Remote error subscribing media item: " + parentId); 1673 } 1674 } 1675 } 1676 1677 @Override unsubscribe(@onNull String parentId, SubscriptionCallback callback)1678 public void unsubscribe(@NonNull String parentId, SubscriptionCallback callback) { 1679 Subscription sub = mSubscriptions.get(parentId); 1680 if (sub == null) { 1681 return; 1682 } 1683 1684 if (mServiceBinderWrapper == null) { 1685 if (callback == null) { 1686 MediaBrowserCompatApi21.unsubscribe(mBrowserObj, parentId); 1687 } else { 1688 final List<SubscriptionCallback> callbacks = sub.getCallbacks(); 1689 final List<Bundle> optionsList = sub.getOptionsList(); 1690 for (int i = callbacks.size() - 1; i >= 0; --i) { 1691 if (callbacks.get(i) == callback) { 1692 callbacks.remove(i); 1693 optionsList.remove(i); 1694 } 1695 } 1696 if (callbacks.size() == 0) { 1697 MediaBrowserCompatApi21.unsubscribe(mBrowserObj, parentId); 1698 } 1699 } 1700 } else { 1701 // Tell the service if necessary. 1702 try { 1703 if (callback == null) { 1704 mServiceBinderWrapper.removeSubscription(parentId, null, 1705 mCallbacksMessenger); 1706 } else { 1707 final List<SubscriptionCallback> callbacks = sub.getCallbacks(); 1708 final List<Bundle> optionsList = sub.getOptionsList(); 1709 for (int i = callbacks.size() - 1; i >= 0; --i) { 1710 if (callbacks.get(i) == callback) { 1711 mServiceBinderWrapper.removeSubscription( 1712 parentId, callback.mToken, mCallbacksMessenger); 1713 callbacks.remove(i); 1714 optionsList.remove(i); 1715 } 1716 } 1717 } 1718 } catch (RemoteException ex) { 1719 // Process is crashing. We will disconnect, and upon reconnect we will 1720 // automatically reregister. So nothing to do here. 1721 Log.d(TAG, "removeSubscription failed with RemoteException parentId=" 1722 + parentId); 1723 } 1724 } 1725 1726 if (sub.isEmpty() || callback == null) { 1727 mSubscriptions.remove(parentId); 1728 } 1729 } 1730 1731 @Override getItem(@onNull final String mediaId, @NonNull final ItemCallback cb)1732 public void getItem(@NonNull final String mediaId, @NonNull final ItemCallback cb) { 1733 if (TextUtils.isEmpty(mediaId)) { 1734 throw new IllegalArgumentException("mediaId is empty"); 1735 } 1736 if (cb == null) { 1737 throw new IllegalArgumentException("cb is null"); 1738 } 1739 if (!MediaBrowserCompatApi21.isConnected(mBrowserObj)) { 1740 Log.i(TAG, "Not connected, unable to retrieve the MediaItem."); 1741 mHandler.post(new Runnable() { 1742 @Override 1743 public void run() { 1744 cb.onError(mediaId); 1745 } 1746 }); 1747 return; 1748 } 1749 if (mServiceBinderWrapper == null) { 1750 mHandler.post(new Runnable() { 1751 @Override 1752 public void run() { 1753 // Default framework implementation. 1754 cb.onError(mediaId); 1755 } 1756 }); 1757 return; 1758 } 1759 ResultReceiver receiver = new ItemReceiver(mediaId, cb, mHandler); 1760 try { 1761 mServiceBinderWrapper.getMediaItem(mediaId, receiver, mCallbacksMessenger); 1762 } catch (RemoteException e) { 1763 Log.i(TAG, "Remote error getting media item: " + mediaId); 1764 mHandler.post(new Runnable() { 1765 @Override 1766 public void run() { 1767 cb.onError(mediaId); 1768 } 1769 }); 1770 } 1771 } 1772 1773 @Override search(@onNull final String query, final Bundle extras, @NonNull final SearchCallback callback)1774 public void search(@NonNull final String query, final Bundle extras, 1775 @NonNull final SearchCallback callback) { 1776 if (!isConnected()) { 1777 throw new IllegalStateException("search() called while not connected"); 1778 } 1779 if (mServiceBinderWrapper == null) { 1780 Log.i(TAG, "The connected service doesn't support search."); 1781 mHandler.post(new Runnable() { 1782 @Override 1783 public void run() { 1784 // Default framework implementation. 1785 callback.onError(query, extras); 1786 } 1787 }); 1788 return; 1789 } 1790 1791 ResultReceiver receiver = new SearchResultReceiver(query, extras, callback, mHandler); 1792 try { 1793 mServiceBinderWrapper.search(query, extras, receiver, mCallbacksMessenger); 1794 } catch (RemoteException e) { 1795 Log.i(TAG, "Remote error searching items with query: " + query, e); 1796 mHandler.post(new Runnable() { 1797 @Override 1798 public void run() { 1799 callback.onError(query, extras); 1800 } 1801 }); 1802 } 1803 } 1804 1805 @Override sendCustomAction(final String action, final Bundle extras, final CustomActionCallback callback)1806 public void sendCustomAction(final String action, final Bundle extras, 1807 final CustomActionCallback callback) { 1808 if (!isConnected()) { 1809 throw new IllegalStateException("Cannot send a custom action (" + action + ") with " 1810 + "extras " + extras + " because the browser is not connected to the " 1811 + "service."); 1812 } 1813 if (mServiceBinderWrapper == null) { 1814 Log.i(TAG, "The connected service doesn't support sendCustomAction."); 1815 mHandler.post(new Runnable() { 1816 @Override 1817 public void run() { 1818 callback.onError(action, extras, null); 1819 } 1820 }); 1821 } 1822 1823 ResultReceiver receiver = new CustomActionResultReceiver(action, extras, callback, 1824 mHandler); 1825 try { 1826 mServiceBinderWrapper.sendCustomAction(action, extras, receiver, 1827 mCallbacksMessenger); 1828 } catch (RemoteException e) { 1829 Log.i(TAG, "Remote error sending a custom action: action=" + action + ", extras=" 1830 + extras, e); 1831 mHandler.post(new Runnable() { 1832 @Override 1833 public void run() { 1834 callback.onError(action, extras, null); 1835 } 1836 }); 1837 } 1838 } 1839 1840 @Override onConnected()1841 public void onConnected() { 1842 Bundle extras = MediaBrowserCompatApi21.getExtras(mBrowserObj); 1843 if (extras == null) { 1844 return; 1845 } 1846 IBinder serviceBinder = BundleCompat.getBinder(extras, EXTRA_MESSENGER_BINDER); 1847 if (serviceBinder != null) { 1848 mServiceBinderWrapper = new ServiceBinderWrapper(serviceBinder, mRootHints); 1849 mCallbacksMessenger = new Messenger(mHandler); 1850 mHandler.setCallbacksMessenger(mCallbacksMessenger); 1851 try { 1852 mServiceBinderWrapper.registerCallbackMessenger(mCallbacksMessenger); 1853 } catch (RemoteException e) { 1854 Log.i(TAG, "Remote error registering client messenger." ); 1855 } 1856 } 1857 IMediaSession sessionToken = IMediaSession.Stub.asInterface( 1858 BundleCompat.getBinder(extras, EXTRA_SESSION_BINDER)); 1859 if (sessionToken != null) { 1860 mMediaSessionToken = MediaSessionCompat.Token.fromToken( 1861 MediaBrowserCompatApi21.getSessionToken(mBrowserObj), sessionToken); 1862 } 1863 } 1864 1865 @Override onConnectionSuspended()1866 public void onConnectionSuspended() { 1867 mServiceBinderWrapper = null; 1868 mCallbacksMessenger = null; 1869 mMediaSessionToken = null; 1870 mHandler.setCallbacksMessenger(null); 1871 } 1872 1873 @Override onConnectionFailed()1874 public void onConnectionFailed() { 1875 // Do noting 1876 } 1877 1878 @Override onServiceConnected(final Messenger callback, final String root, final MediaSessionCompat.Token session, final Bundle extra)1879 public void onServiceConnected(final Messenger callback, final String root, 1880 final MediaSessionCompat.Token session, final Bundle extra) { 1881 // This method will not be called. 1882 } 1883 1884 @Override onConnectionFailed(Messenger callback)1885 public void onConnectionFailed(Messenger callback) { 1886 // This method will not be called. 1887 } 1888 1889 @Override 1890 @SuppressWarnings("ReferenceEquality") onLoadChildren(Messenger callback, String parentId, List list, Bundle options)1891 public void onLoadChildren(Messenger callback, String parentId, List list, Bundle options) { 1892 if (mCallbacksMessenger != callback) { 1893 return; 1894 } 1895 1896 // Check that the subscription is still subscribed. 1897 Subscription subscription = mSubscriptions.get(parentId); 1898 if (subscription == null) { 1899 if (DEBUG) { 1900 Log.d(TAG, "onLoadChildren for id that isn't subscribed id=" + parentId); 1901 } 1902 return; 1903 } 1904 1905 // Tell the app. 1906 SubscriptionCallback subscriptionCallback = subscription.getCallback(options); 1907 if (subscriptionCallback != null) { 1908 if (options == null) { 1909 if (list == null) { 1910 subscriptionCallback.onError(parentId); 1911 } else { 1912 subscriptionCallback.onChildrenLoaded(parentId, list); 1913 } 1914 } else { 1915 if (list == null) { 1916 subscriptionCallback.onError(parentId, options); 1917 } else { 1918 subscriptionCallback.onChildrenLoaded(parentId, list, options); 1919 } 1920 } 1921 } 1922 } 1923 } 1924 1925 @RequiresApi(23) 1926 static class MediaBrowserImplApi23 extends MediaBrowserImplApi21 { MediaBrowserImplApi23(Context context, ComponentName serviceComponent, ConnectionCallback callback, Bundle rootHints)1927 public MediaBrowserImplApi23(Context context, ComponentName serviceComponent, 1928 ConnectionCallback callback, Bundle rootHints) { 1929 super(context, serviceComponent, callback, rootHints); 1930 } 1931 1932 @Override getItem(@onNull final String mediaId, @NonNull final ItemCallback cb)1933 public void getItem(@NonNull final String mediaId, @NonNull final ItemCallback cb) { 1934 if (mServiceBinderWrapper == null) { 1935 MediaBrowserCompatApi23.getItem(mBrowserObj, mediaId, cb.mItemCallbackObj); 1936 } else { 1937 super.getItem(mediaId, cb); 1938 } 1939 } 1940 } 1941 1942 // TODO: Rename to MediaBrowserImplApi26 once O is released 1943 @RequiresApi(26) 1944 static class MediaBrowserImplApi24 extends MediaBrowserImplApi23 { MediaBrowserImplApi24(Context context, ComponentName serviceComponent, ConnectionCallback callback, Bundle rootHints)1945 public MediaBrowserImplApi24(Context context, ComponentName serviceComponent, 1946 ConnectionCallback callback, Bundle rootHints) { 1947 super(context, serviceComponent, callback, rootHints); 1948 } 1949 1950 @Override subscribe(@onNull String parentId, @NonNull Bundle options, @NonNull SubscriptionCallback callback)1951 public void subscribe(@NonNull String parentId, @NonNull Bundle options, 1952 @NonNull SubscriptionCallback callback) { 1953 if (options == null) { 1954 MediaBrowserCompatApi21.subscribe( 1955 mBrowserObj, parentId, callback.mSubscriptionCallbackObj); 1956 } else { 1957 MediaBrowserCompatApi24.subscribe( 1958 mBrowserObj, parentId, options, callback.mSubscriptionCallbackObj); 1959 } 1960 } 1961 1962 @Override unsubscribe(@onNull String parentId, SubscriptionCallback callback)1963 public void unsubscribe(@NonNull String parentId, SubscriptionCallback callback) { 1964 if (callback == null) { 1965 MediaBrowserCompatApi21.unsubscribe(mBrowserObj, parentId); 1966 } else { 1967 MediaBrowserCompatApi24.unsubscribe(mBrowserObj, parentId, 1968 callback.mSubscriptionCallbackObj); 1969 } 1970 } 1971 } 1972 1973 private static class Subscription { 1974 private final List<SubscriptionCallback> mCallbacks; 1975 private final List<Bundle> mOptionsList; 1976 Subscription()1977 public Subscription() { 1978 mCallbacks = new ArrayList<>(); 1979 mOptionsList = new ArrayList<>(); 1980 } 1981 isEmpty()1982 public boolean isEmpty() { 1983 return mCallbacks.isEmpty(); 1984 } 1985 getOptionsList()1986 public List<Bundle> getOptionsList() { 1987 return mOptionsList; 1988 } 1989 getCallbacks()1990 public List<SubscriptionCallback> getCallbacks() { 1991 return mCallbacks; 1992 } 1993 getCallback(Bundle options)1994 public SubscriptionCallback getCallback(Bundle options) { 1995 for (int i = 0; i < mOptionsList.size(); ++i) { 1996 if (MediaBrowserCompatUtils.areSameOptions(mOptionsList.get(i), options)) { 1997 return mCallbacks.get(i); 1998 } 1999 } 2000 return null; 2001 } 2002 putCallback(Bundle options, SubscriptionCallback callback)2003 public void putCallback(Bundle options, SubscriptionCallback callback) { 2004 for (int i = 0; i < mOptionsList.size(); ++i) { 2005 if (MediaBrowserCompatUtils.areSameOptions(mOptionsList.get(i), options)) { 2006 mCallbacks.set(i, callback); 2007 return; 2008 } 2009 } 2010 mCallbacks.add(callback); 2011 mOptionsList.add(options); 2012 } 2013 } 2014 2015 private static class CallbackHandler extends Handler { 2016 private final WeakReference<MediaBrowserServiceCallbackImpl> mCallbackImplRef; 2017 private WeakReference<Messenger> mCallbacksMessengerRef; 2018 CallbackHandler(MediaBrowserServiceCallbackImpl callbackImpl)2019 CallbackHandler(MediaBrowserServiceCallbackImpl callbackImpl) { 2020 super(); 2021 mCallbackImplRef = new WeakReference<>(callbackImpl); 2022 } 2023 2024 @Override handleMessage(Message msg)2025 public void handleMessage(Message msg) { 2026 if (mCallbacksMessengerRef == null || mCallbacksMessengerRef.get() == null || 2027 mCallbackImplRef.get() == null) { 2028 return; 2029 } 2030 Bundle data = msg.getData(); 2031 data.setClassLoader(MediaSessionCompat.class.getClassLoader()); 2032 MediaBrowserServiceCallbackImpl serviceCallback = mCallbackImplRef.get(); 2033 Messenger callbacksMessenger = mCallbacksMessengerRef.get(); 2034 try { 2035 switch (msg.what) { 2036 case SERVICE_MSG_ON_CONNECT: 2037 serviceCallback.onServiceConnected(callbacksMessenger, 2038 data.getString(DATA_MEDIA_ITEM_ID), 2039 (MediaSessionCompat.Token) data.getParcelable( 2040 DATA_MEDIA_SESSION_TOKEN), 2041 data.getBundle(DATA_ROOT_HINTS)); 2042 break; 2043 case SERVICE_MSG_ON_CONNECT_FAILED: 2044 serviceCallback.onConnectionFailed(callbacksMessenger); 2045 break; 2046 case SERVICE_MSG_ON_LOAD_CHILDREN: 2047 serviceCallback.onLoadChildren(callbacksMessenger, 2048 data.getString(DATA_MEDIA_ITEM_ID), 2049 data.getParcelableArrayList(DATA_MEDIA_ITEM_LIST), 2050 data.getBundle(DATA_OPTIONS)); 2051 break; 2052 default: 2053 Log.w(TAG, "Unhandled message: " + msg 2054 + "\n Client version: " + CLIENT_VERSION_CURRENT 2055 + "\n Service version: " + msg.arg1); 2056 } 2057 } catch (BadParcelableException e) { 2058 // Do not print the exception here, since it is already done by the Parcel class. 2059 Log.e(TAG, "Could not unparcel the data."); 2060 // If an error happened while connecting, disconnect from the service. 2061 if (msg.what == SERVICE_MSG_ON_CONNECT) { 2062 serviceCallback.onConnectionFailed(callbacksMessenger); 2063 } 2064 } 2065 } 2066 setCallbacksMessenger(Messenger callbacksMessenger)2067 void setCallbacksMessenger(Messenger callbacksMessenger) { 2068 mCallbacksMessengerRef = new WeakReference<>(callbacksMessenger); 2069 } 2070 } 2071 2072 private static class ServiceBinderWrapper { 2073 private Messenger mMessenger; 2074 private Bundle mRootHints; 2075 ServiceBinderWrapper(IBinder target, Bundle rootHints)2076 public ServiceBinderWrapper(IBinder target, Bundle rootHints) { 2077 mMessenger = new Messenger(target); 2078 mRootHints = rootHints; 2079 } 2080 connect(Context context, Messenger callbacksMessenger)2081 void connect(Context context, Messenger callbacksMessenger) 2082 throws RemoteException { 2083 Bundle data = new Bundle(); 2084 data.putString(DATA_PACKAGE_NAME, context.getPackageName()); 2085 data.putBundle(DATA_ROOT_HINTS, mRootHints); 2086 sendRequest(CLIENT_MSG_CONNECT, data, callbacksMessenger); 2087 } 2088 disconnect(Messenger callbacksMessenger)2089 void disconnect(Messenger callbacksMessenger) throws RemoteException { 2090 sendRequest(CLIENT_MSG_DISCONNECT, null, callbacksMessenger); 2091 } 2092 addSubscription(String parentId, IBinder callbackToken, Bundle options, Messenger callbacksMessenger)2093 void addSubscription(String parentId, IBinder callbackToken, Bundle options, 2094 Messenger callbacksMessenger) 2095 throws RemoteException { 2096 Bundle data = new Bundle(); 2097 data.putString(DATA_MEDIA_ITEM_ID, parentId); 2098 BundleCompat.putBinder(data, DATA_CALLBACK_TOKEN, callbackToken); 2099 data.putBundle(DATA_OPTIONS, options); 2100 sendRequest(CLIENT_MSG_ADD_SUBSCRIPTION, data, callbacksMessenger); 2101 } 2102 removeSubscription(String parentId, IBinder callbackToken, Messenger callbacksMessenger)2103 void removeSubscription(String parentId, IBinder callbackToken, 2104 Messenger callbacksMessenger) 2105 throws RemoteException { 2106 Bundle data = new Bundle(); 2107 data.putString(DATA_MEDIA_ITEM_ID, parentId); 2108 BundleCompat.putBinder(data, DATA_CALLBACK_TOKEN, callbackToken); 2109 sendRequest(CLIENT_MSG_REMOVE_SUBSCRIPTION, data, callbacksMessenger); 2110 } 2111 getMediaItem(String mediaId, ResultReceiver receiver, Messenger callbacksMessenger)2112 void getMediaItem(String mediaId, ResultReceiver receiver, Messenger callbacksMessenger) 2113 throws RemoteException { 2114 Bundle data = new Bundle(); 2115 data.putString(DATA_MEDIA_ITEM_ID, mediaId); 2116 data.putParcelable(DATA_RESULT_RECEIVER, receiver); 2117 sendRequest(CLIENT_MSG_GET_MEDIA_ITEM, data, callbacksMessenger); 2118 } 2119 registerCallbackMessenger(Messenger callbackMessenger)2120 void registerCallbackMessenger(Messenger callbackMessenger) throws RemoteException { 2121 Bundle data = new Bundle(); 2122 data.putBundle(DATA_ROOT_HINTS, mRootHints); 2123 sendRequest(CLIENT_MSG_REGISTER_CALLBACK_MESSENGER, data, callbackMessenger); 2124 } 2125 unregisterCallbackMessenger(Messenger callbackMessenger)2126 void unregisterCallbackMessenger(Messenger callbackMessenger) throws RemoteException { 2127 sendRequest(CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER, null, callbackMessenger); 2128 } 2129 search(String query, Bundle extras, ResultReceiver receiver, Messenger callbacksMessenger)2130 void search(String query, Bundle extras, ResultReceiver receiver, 2131 Messenger callbacksMessenger) throws RemoteException { 2132 Bundle data = new Bundle(); 2133 data.putString(DATA_SEARCH_QUERY, query); 2134 data.putBundle(DATA_SEARCH_EXTRAS, extras); 2135 data.putParcelable(DATA_RESULT_RECEIVER, receiver); 2136 sendRequest(CLIENT_MSG_SEARCH, data, callbacksMessenger); 2137 } 2138 sendCustomAction(String action, Bundle extras, ResultReceiver receiver, Messenger callbacksMessenger)2139 void sendCustomAction(String action, Bundle extras, ResultReceiver receiver, 2140 Messenger callbacksMessenger) throws RemoteException { 2141 Bundle data = new Bundle(); 2142 data.putString(DATA_CUSTOM_ACTION, action); 2143 data.putBundle(DATA_CUSTOM_ACTION_EXTRAS, extras); 2144 data.putParcelable(DATA_RESULT_RECEIVER, receiver); 2145 sendRequest(CLIENT_MSG_SEND_CUSTOM_ACTION, data, callbacksMessenger); 2146 } 2147 sendRequest(int what, Bundle data, Messenger cbMessenger)2148 private void sendRequest(int what, Bundle data, Messenger cbMessenger) 2149 throws RemoteException { 2150 Message msg = Message.obtain(); 2151 msg.what = what; 2152 msg.arg1 = CLIENT_VERSION_CURRENT; 2153 msg.setData(data); 2154 msg.replyTo = cbMessenger; 2155 mMessenger.send(msg); 2156 } 2157 } 2158 2159 private static class ItemReceiver extends ResultReceiver { 2160 private final String mMediaId; 2161 private final ItemCallback mCallback; 2162 ItemReceiver(String mediaId, ItemCallback callback, Handler handler)2163 ItemReceiver(String mediaId, ItemCallback callback, Handler handler) { 2164 super(handler); 2165 mMediaId = mediaId; 2166 mCallback = callback; 2167 } 2168 2169 @Override onReceiveResult(int resultCode, Bundle resultData)2170 protected void onReceiveResult(int resultCode, Bundle resultData) { 2171 if (resultData != null) { 2172 resultData.setClassLoader(MediaBrowserCompat.class.getClassLoader()); 2173 } 2174 if (resultCode != MediaBrowserServiceCompat.RESULT_OK || resultData == null 2175 || !resultData.containsKey(MediaBrowserServiceCompat.KEY_MEDIA_ITEM)) { 2176 mCallback.onError(mMediaId); 2177 return; 2178 } 2179 Parcelable item = resultData.getParcelable(MediaBrowserServiceCompat.KEY_MEDIA_ITEM); 2180 if (item == null || item instanceof MediaItem) { 2181 mCallback.onItemLoaded((MediaItem) item); 2182 } else { 2183 mCallback.onError(mMediaId); 2184 } 2185 } 2186 } 2187 2188 private static class SearchResultReceiver extends ResultReceiver { 2189 private final String mQuery; 2190 private final Bundle mExtras; 2191 private final SearchCallback mCallback; 2192 SearchResultReceiver(String query, Bundle extras, SearchCallback callback, Handler handler)2193 SearchResultReceiver(String query, Bundle extras, SearchCallback callback, 2194 Handler handler) { 2195 super(handler); 2196 mQuery = query; 2197 mExtras = extras; 2198 mCallback = callback; 2199 } 2200 2201 @Override onReceiveResult(int resultCode, Bundle resultData)2202 protected void onReceiveResult(int resultCode, Bundle resultData) { 2203 if (resultData != null) { 2204 resultData.setClassLoader(MediaBrowserCompat.class.getClassLoader()); 2205 } 2206 if (resultCode != MediaBrowserServiceCompat.RESULT_OK || resultData == null 2207 || !resultData.containsKey(MediaBrowserServiceCompat.KEY_SEARCH_RESULTS)) { 2208 mCallback.onError(mQuery, mExtras); 2209 return; 2210 } 2211 Parcelable[] items = resultData.getParcelableArray( 2212 MediaBrowserServiceCompat.KEY_SEARCH_RESULTS); 2213 List<MediaItem> results = null; 2214 if (items != null) { 2215 results = new ArrayList<>(); 2216 for (Parcelable item : items) { 2217 results.add((MediaItem) item); 2218 } 2219 } 2220 mCallback.onSearchResult(mQuery, mExtras, results); 2221 } 2222 } 2223 2224 private static class CustomActionResultReceiver extends ResultReceiver { 2225 private final String mAction; 2226 private final Bundle mExtras; 2227 private final CustomActionCallback mCallback; 2228 CustomActionResultReceiver(String action, Bundle extras, CustomActionCallback callback, Handler handler)2229 CustomActionResultReceiver(String action, Bundle extras, CustomActionCallback callback, 2230 Handler handler) { 2231 super(handler); 2232 mAction = action; 2233 mExtras = extras; 2234 mCallback = callback; 2235 } 2236 2237 @Override onReceiveResult(int resultCode, Bundle resultData)2238 protected void onReceiveResult(int resultCode, Bundle resultData) { 2239 if (mCallback == null) { 2240 return; 2241 } 2242 switch (resultCode) { 2243 case MediaBrowserServiceCompat.RESULT_PROGRESS_UPDATE: 2244 mCallback.onProgressUpdate(mAction, mExtras, resultData); 2245 break; 2246 case MediaBrowserServiceCompat.RESULT_OK: 2247 mCallback.onResult(mAction, mExtras, resultData); 2248 break; 2249 case MediaBrowserServiceCompat.RESULT_ERROR: 2250 mCallback.onError(mAction, mExtras, resultData); 2251 break; 2252 default: 2253 Log.w(TAG, "Unknown result code: " + resultCode + " (extras=" + mExtras 2254 + ", resultData=" + resultData + ")"); 2255 break; 2256 } 2257 } 2258 } 2259 } 2260