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