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