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