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