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