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