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