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