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