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