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