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