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