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