MediaBrowserCompat.java revision 082f92c8357f91e7c12d038d71258d3bd61cb662
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 sub.putCallback(options, callback); 983 984 // If we are connected, tell the service that we are watching. If we aren't 985 // connected, the service will be told when we connect. 986 if (mState == CONNECT_STATE_CONNECTED) { 987 try { 988 mServiceBinderWrapper.addSubscription(parentId, callback.mToken, options, 989 mCallbacksMessenger); 990 } catch (RemoteException e) { 991 // Process is crashing. We will disconnect, and upon reconnect we will 992 // automatically reregister. So nothing to do here. 993 Log.d(TAG, "addSubscription failed with RemoteException parentId=" + parentId); 994 } 995 } 996 } 997 998 @Override 999 public void unsubscribe(@NonNull String parentId, SubscriptionCallback callback) { 1000 Subscription sub = mSubscriptions.get(parentId); 1001 if (sub == null) { 1002 return; 1003 } 1004 1005 // Tell the service if necessary. 1006 try { 1007 if (callback == null) { 1008 if (mState == CONNECT_STATE_CONNECTED) { 1009 mServiceBinderWrapper.removeSubscription(parentId, null, 1010 mCallbacksMessenger); 1011 } 1012 } else { 1013 final List<SubscriptionCallback> callbacks = sub.getCallbacks(); 1014 final List<Bundle> optionsList = sub.getOptionsList(); 1015 for (int i = callbacks.size() - 1; i >= 0; --i) { 1016 if (callbacks.get(i) == callback) { 1017 if (mState == CONNECT_STATE_CONNECTED) { 1018 mServiceBinderWrapper.removeSubscription( 1019 parentId, callback.mToken, mCallbacksMessenger); 1020 } 1021 callbacks.remove(i); 1022 optionsList.remove(i); 1023 } 1024 } 1025 } 1026 } catch (RemoteException ex) { 1027 // Process is crashing. We will disconnect, and upon reconnect we will 1028 // automatically reregister. So nothing to do here. 1029 Log.d(TAG, "removeSubscription failed with RemoteException parentId=" + parentId); 1030 } 1031 1032 if (sub.isEmpty() || callback == null) { 1033 mSubscriptions.remove(parentId); 1034 } 1035 } 1036 1037 @Override 1038 public void getItem(@NonNull final String mediaId, @NonNull final ItemCallback cb) { 1039 if (TextUtils.isEmpty(mediaId)) { 1040 throw new IllegalArgumentException("mediaId is empty"); 1041 } 1042 if (cb == null) { 1043 throw new IllegalArgumentException("cb is null"); 1044 } 1045 if (mState != CONNECT_STATE_CONNECTED) { 1046 Log.i(TAG, "Not connected, unable to retrieve the MediaItem."); 1047 mHandler.post(new Runnable() { 1048 @Override 1049 public void run() { 1050 cb.onError(mediaId); 1051 } 1052 }); 1053 return; 1054 } 1055 ResultReceiver receiver = new ItemReceiver(mediaId, cb, mHandler); 1056 try { 1057 mServiceBinderWrapper.getMediaItem(mediaId, receiver, mCallbacksMessenger); 1058 } catch (RemoteException e) { 1059 Log.i(TAG, "Remote error getting media item."); 1060 mHandler.post(new Runnable() { 1061 @Override 1062 public void run() { 1063 cb.onError(mediaId); 1064 } 1065 }); 1066 } 1067 } 1068 1069 @Override 1070 public void onServiceConnected(final Messenger callback, final String root, 1071 final MediaSessionCompat.Token session, final Bundle extra) { 1072 // Check to make sure there hasn't been a disconnect or a different ServiceConnection. 1073 if (!isCurrent(callback, "onConnect")) { 1074 return; 1075 } 1076 // Don't allow them to call us twice. 1077 if (mState != CONNECT_STATE_CONNECTING) { 1078 Log.w(TAG, "onConnect from service while mState=" + getStateLabel(mState) 1079 + "... ignoring"); 1080 return; 1081 } 1082 mRootId = root; 1083 mMediaSessionToken = session; 1084 mExtras = extra; 1085 mState = CONNECT_STATE_CONNECTED; 1086 1087 if (DEBUG) { 1088 Log.d(TAG, "ServiceCallbacks.onConnect..."); 1089 dump(); 1090 } 1091 mCallback.onConnected(); 1092 1093 // we may receive some subscriptions before we are connected, so re-subscribe 1094 // everything now 1095 try { 1096 for (Map.Entry<String, Subscription> subscriptionEntry 1097 : mSubscriptions.entrySet()) { 1098 String id = subscriptionEntry.getKey(); 1099 Subscription sub = subscriptionEntry.getValue(); 1100 List<SubscriptionCallback> callbackList = sub.getCallbacks(); 1101 List<Bundle> optionsList = sub.getOptionsList(); 1102 for (int i = 0; i < callbackList.size(); ++i) { 1103 mServiceBinderWrapper.addSubscription(id, callbackList.get(i).mToken, 1104 optionsList.get(i), mCallbacksMessenger); 1105 } 1106 } 1107 } catch (RemoteException ex) { 1108 // Process is crashing. We will disconnect, and upon reconnect we will 1109 // automatically reregister. So nothing to do here. 1110 Log.d(TAG, "addSubscription failed with RemoteException."); 1111 } 1112 } 1113 1114 @Override 1115 public void onConnectionFailed(final Messenger callback) { 1116 Log.e(TAG, "onConnectFailed for " + mServiceComponent); 1117 1118 // Check to make sure there hasn't been a disconnect or a different ServiceConnection. 1119 if (!isCurrent(callback, "onConnectFailed")) { 1120 return; 1121 } 1122 // Don't allow them to call us twice. 1123 if (mState != CONNECT_STATE_CONNECTING) { 1124 Log.w(TAG, "onConnect from service while mState=" + getStateLabel(mState) 1125 + "... ignoring"); 1126 return; 1127 } 1128 1129 // Clean up 1130 forceCloseConnection(); 1131 1132 // Tell the app. 1133 mCallback.onConnectionFailed(); 1134 } 1135 1136 @Override 1137 public void onLoadChildren(final Messenger callback, final String parentId, 1138 final List list, final Bundle options) { 1139 // Check that there hasn't been a disconnect or a different ServiceConnection. 1140 if (!isCurrent(callback, "onLoadChildren")) { 1141 return; 1142 } 1143 1144 if (DEBUG) { 1145 Log.d(TAG, "onLoadChildren for " + mServiceComponent + " id=" + parentId); 1146 } 1147 1148 // Check that the subscription is still subscribed. 1149 final Subscription subscription = mSubscriptions.get(parentId); 1150 if (subscription == null) { 1151 if (DEBUG) { 1152 Log.d(TAG, "onLoadChildren for id that isn't subscribed id=" + parentId); 1153 } 1154 return; 1155 } 1156 1157 // Tell the app. 1158 SubscriptionCallback subscriptionCallback = subscription.getCallback(options); 1159 if (subscriptionCallback != null) { 1160 if (options == null) { 1161 if (list == null) { 1162 subscriptionCallback.onError(parentId); 1163 } else { 1164 subscriptionCallback.onChildrenLoaded(parentId, list); 1165 } 1166 } else { 1167 if (list == null) { 1168 subscriptionCallback.onError(parentId, options); 1169 } else { 1170 subscriptionCallback.onChildrenLoaded(parentId, list, options); 1171 } 1172 } 1173 } 1174 } 1175 1176 /** 1177 * For debugging. 1178 */ 1179 private static String getStateLabel(int state) { 1180 switch (state) { 1181 case CONNECT_STATE_DISCONNECTED: 1182 return "CONNECT_STATE_DISCONNECTED"; 1183 case CONNECT_STATE_CONNECTING: 1184 return "CONNECT_STATE_CONNECTING"; 1185 case CONNECT_STATE_CONNECTED: 1186 return "CONNECT_STATE_CONNECTED"; 1187 case CONNECT_STATE_SUSPENDED: 1188 return "CONNECT_STATE_SUSPENDED"; 1189 default: 1190 return "UNKNOWN/" + state; 1191 } 1192 } 1193 1194 /** 1195 * Return true if {@code callback} is the current ServiceCallbacks. Also logs if it's not. 1196 */ 1197 private boolean isCurrent(Messenger callback, String funcName) { 1198 if (mCallbacksMessenger != callback) { 1199 if (mState != CONNECT_STATE_DISCONNECTED) { 1200 Log.i(TAG, funcName + " for " + mServiceComponent + " with mCallbacksMessenger=" 1201 + mCallbacksMessenger + " this=" + this); 1202 } 1203 return false; 1204 } 1205 return true; 1206 } 1207 1208 /** 1209 * Log internal state. 1210 */ 1211 void dump() { 1212 Log.d(TAG, "MediaBrowserCompat..."); 1213 Log.d(TAG, " mServiceComponent=" + mServiceComponent); 1214 Log.d(TAG, " mCallback=" + mCallback); 1215 Log.d(TAG, " mRootHints=" + mRootHints); 1216 Log.d(TAG, " mState=" + getStateLabel(mState)); 1217 Log.d(TAG, " mServiceConnection=" + mServiceConnection); 1218 Log.d(TAG, " mServiceBinderWrapper=" + mServiceBinderWrapper); 1219 Log.d(TAG, " mCallbacksMessenger=" + mCallbacksMessenger); 1220 Log.d(TAG, " mRootId=" + mRootId); 1221 Log.d(TAG, " mMediaSessionToken=" + mMediaSessionToken); 1222 } 1223 1224 /** 1225 * ServiceConnection to the other app. 1226 */ 1227 private class MediaServiceConnection implements ServiceConnection { 1228 MediaServiceConnection() { 1229 } 1230 1231 @Override 1232 public void onServiceConnected(final ComponentName name, final IBinder binder) { 1233 postOrRun(new Runnable() { 1234 @Override 1235 public void run() { 1236 if (DEBUG) { 1237 Log.d(TAG, "MediaServiceConnection.onServiceConnected name=" + name 1238 + " binder=" + binder); 1239 dump(); 1240 } 1241 1242 // Make sure we are still the current connection, and that they haven't 1243 // called disconnect(). 1244 if (!isCurrent("onServiceConnected")) { 1245 return; 1246 } 1247 1248 // Save their binder 1249 mServiceBinderWrapper = new ServiceBinderWrapper(binder, mRootHints); 1250 1251 // We make a new mServiceCallbacks each time we connect so that we can drop 1252 // responses from previous connections. 1253 mCallbacksMessenger = new Messenger(mHandler); 1254 mHandler.setCallbacksMessenger(mCallbacksMessenger); 1255 1256 mState = CONNECT_STATE_CONNECTING; 1257 1258 // Call connect, which is async. When we get a response from that we will 1259 // say that we're connected. 1260 try { 1261 if (DEBUG) { 1262 Log.d(TAG, "ServiceCallbacks.onConnect..."); 1263 dump(); 1264 } 1265 mServiceBinderWrapper.connect(mContext, mCallbacksMessenger); 1266 } catch (RemoteException ex) { 1267 // Connect failed, which isn't good. But the auto-reconnect on the 1268 // service will take over and we will come back. We will also get the 1269 // onServiceDisconnected, which has all the cleanup code. So let that 1270 // do it. 1271 Log.w(TAG, "RemoteException during connect for " + mServiceComponent); 1272 if (DEBUG) { 1273 Log.d(TAG, "ServiceCallbacks.onConnect..."); 1274 dump(); 1275 } 1276 } 1277 } 1278 }); 1279 } 1280 1281 @Override 1282 public void onServiceDisconnected(final ComponentName name) { 1283 postOrRun(new Runnable() { 1284 @Override 1285 public void run() { 1286 if (DEBUG) { 1287 Log.d(TAG, "MediaServiceConnection.onServiceDisconnected name=" + name 1288 + " this=" + this + " mServiceConnection=" + 1289 mServiceConnection); 1290 dump(); 1291 } 1292 1293 // Make sure we are still the current connection, and that they haven't 1294 // called disconnect(). 1295 if (!isCurrent("onServiceDisconnected")) { 1296 return; 1297 } 1298 1299 // Clear out what we set in onServiceConnected 1300 mServiceBinderWrapper = null; 1301 mCallbacksMessenger = null; 1302 mHandler.setCallbacksMessenger(null); 1303 1304 // And tell the app that it's suspended. 1305 mState = CONNECT_STATE_SUSPENDED; 1306 mCallback.onConnectionSuspended(); 1307 } 1308 }); 1309 } 1310 1311 private void postOrRun(Runnable r) { 1312 if (Thread.currentThread() == mHandler.getLooper().getThread()) { 1313 r.run(); 1314 } else { 1315 mHandler.post(r); 1316 } 1317 } 1318 1319 /** 1320 * Return true if this is the current ServiceConnection. Also logs if it's not. 1321 */ 1322 boolean isCurrent(String funcName) { 1323 if (mServiceConnection != this) { 1324 if (mState != CONNECT_STATE_DISCONNECTED) { 1325 // Check mState, because otherwise this log is noisy. 1326 Log.i(TAG, funcName + " for " + mServiceComponent + 1327 " with mServiceConnection=" + mServiceConnection + " this=" + this); 1328 } 1329 return false; 1330 } 1331 return true; 1332 } 1333 } 1334 } 1335 1336 static class MediaBrowserImplApi21 implements MediaBrowserImpl, MediaBrowserServiceCallbackImpl, 1337 ConnectionCallback.ConnectionCallbackInternal { 1338 protected final Object mBrowserObj; 1339 protected final Bundle mRootHints; 1340 protected final CallbackHandler mHandler = new CallbackHandler(this); 1341 private final ArrayMap<String, Subscription> mSubscriptions = new ArrayMap<>(); 1342 1343 protected ServiceBinderWrapper mServiceBinderWrapper; 1344 protected Messenger mCallbacksMessenger; 1345 1346 public MediaBrowserImplApi21(Context context, ComponentName serviceComponent, 1347 ConnectionCallback callback, Bundle rootHints) { 1348 // Do not send the client version for API 25 and higher, since we don't need to use 1349 // EXTRA_MESSENGER_BINDER for API 24 and higher. 1350 if (Build.VERSION.SDK_INT < 25) { 1351 if (rootHints == null) { 1352 rootHints = new Bundle(); 1353 } 1354 rootHints.putInt(EXTRA_CLIENT_VERSION, CLIENT_VERSION_CURRENT); 1355 mRootHints = new Bundle(rootHints); 1356 } else { 1357 mRootHints = rootHints == null ? null : new Bundle(rootHints); 1358 } 1359 callback.setInternalConnectionCallback(this); 1360 mBrowserObj = MediaBrowserCompatApi21.createBrowser(context, serviceComponent, 1361 callback.mConnectionCallbackObj, mRootHints); 1362 } 1363 1364 @Override 1365 public void connect() { 1366 MediaBrowserCompatApi21.connect(mBrowserObj); 1367 } 1368 1369 @Override 1370 public void disconnect() { 1371 if (mServiceBinderWrapper != null && mCallbacksMessenger != null) { 1372 try { 1373 mServiceBinderWrapper.unregisterCallbackMessenger(mCallbacksMessenger); 1374 } catch (RemoteException e) { 1375 Log.i(TAG, "Remote error unregistering client messenger." ); 1376 } 1377 } 1378 MediaBrowserCompatApi21.disconnect(mBrowserObj); 1379 } 1380 1381 @Override 1382 public boolean isConnected() { 1383 return MediaBrowserCompatApi21.isConnected(mBrowserObj); 1384 } 1385 1386 @Override 1387 public ComponentName getServiceComponent() { 1388 return MediaBrowserCompatApi21.getServiceComponent(mBrowserObj); 1389 } 1390 1391 @NonNull 1392 @Override 1393 public String getRoot() { 1394 return MediaBrowserCompatApi21.getRoot(mBrowserObj); 1395 } 1396 1397 @Nullable 1398 @Override 1399 public Bundle getExtras() { 1400 return MediaBrowserCompatApi21.getExtras(mBrowserObj); 1401 } 1402 1403 @NonNull 1404 @Override 1405 public MediaSessionCompat.Token getSessionToken() { 1406 return MediaSessionCompat.Token.fromToken( 1407 MediaBrowserCompatApi21.getSessionToken(mBrowserObj)); 1408 } 1409 1410 @Override 1411 public void subscribe(@NonNull final String parentId, final Bundle options, 1412 @NonNull final SubscriptionCallback callback) { 1413 // Update or create the subscription. 1414 Subscription sub = mSubscriptions.get(parentId); 1415 if (sub == null) { 1416 sub = new Subscription(); 1417 mSubscriptions.put(parentId, sub); 1418 } 1419 callback.setSubscription(sub); 1420 sub.putCallback(options, callback); 1421 1422 if (mServiceBinderWrapper == null) { 1423 MediaBrowserCompatApi21.subscribe( 1424 mBrowserObj, parentId, callback.mSubscriptionCallbackObj); 1425 } else { 1426 try { 1427 mServiceBinderWrapper.addSubscription( 1428 parentId, callback.mToken, options, mCallbacksMessenger); 1429 } catch (RemoteException e) { 1430 // Process is crashing. We will disconnect, and upon reconnect we will 1431 // automatically reregister. So nothing to do here. 1432 Log.i(TAG, "Remote error subscribing media item: " + parentId); 1433 } 1434 } 1435 } 1436 1437 @Override 1438 public void unsubscribe(@NonNull String parentId, SubscriptionCallback callback) { 1439 Subscription sub = mSubscriptions.get(parentId); 1440 if (sub == null) { 1441 return; 1442 } 1443 1444 if (mServiceBinderWrapper == null) { 1445 if (callback == null) { 1446 MediaBrowserCompatApi21.unsubscribe(mBrowserObj, parentId); 1447 } else { 1448 final List<SubscriptionCallback> callbacks = sub.getCallbacks(); 1449 final List<Bundle> optionsList = sub.getOptionsList(); 1450 for (int i = callbacks.size() - 1; i >= 0; --i) { 1451 if (callbacks.get(i) == callback) { 1452 callbacks.remove(i); 1453 optionsList.remove(i); 1454 } 1455 } 1456 if (callbacks.size() == 0) { 1457 MediaBrowserCompatApi21.unsubscribe(mBrowserObj, parentId); 1458 } 1459 } 1460 } else { 1461 // Tell the service if necessary. 1462 try { 1463 if (callback == null) { 1464 mServiceBinderWrapper.removeSubscription(parentId, null, 1465 mCallbacksMessenger); 1466 } else { 1467 final List<SubscriptionCallback> callbacks = sub.getCallbacks(); 1468 final List<Bundle> optionsList = sub.getOptionsList(); 1469 for (int i = callbacks.size() - 1; i >= 0; --i) { 1470 if (callbacks.get(i) == callback) { 1471 mServiceBinderWrapper.removeSubscription( 1472 parentId, callback.mToken, mCallbacksMessenger); 1473 callbacks.remove(i); 1474 optionsList.remove(i); 1475 } 1476 } 1477 } 1478 } catch (RemoteException ex) { 1479 // Process is crashing. We will disconnect, and upon reconnect we will 1480 // automatically reregister. So nothing to do here. 1481 Log.d(TAG, "removeSubscription failed with RemoteException parentId=" 1482 + parentId); 1483 } 1484 } 1485 1486 if (sub.isEmpty() || callback == null) { 1487 mSubscriptions.remove(parentId); 1488 } 1489 } 1490 1491 @Override 1492 public void getItem(@NonNull final String mediaId, @NonNull final ItemCallback cb) { 1493 if (TextUtils.isEmpty(mediaId)) { 1494 throw new IllegalArgumentException("mediaId is empty"); 1495 } 1496 if (cb == null) { 1497 throw new IllegalArgumentException("cb is null"); 1498 } 1499 if (!MediaBrowserCompatApi21.isConnected(mBrowserObj)) { 1500 Log.i(TAG, "Not connected, unable to retrieve the MediaItem."); 1501 mHandler.post(new Runnable() { 1502 @Override 1503 public void run() { 1504 cb.onError(mediaId); 1505 } 1506 }); 1507 return; 1508 } 1509 if (mServiceBinderWrapper == null) { 1510 mHandler.post(new Runnable() { 1511 @Override 1512 public void run() { 1513 // Default framework implementation. 1514 cb.onError(mediaId); 1515 } 1516 }); 1517 return; 1518 } 1519 ResultReceiver receiver = new ItemReceiver(mediaId, cb, mHandler); 1520 try { 1521 mServiceBinderWrapper.getMediaItem(mediaId, receiver, mCallbacksMessenger); 1522 } catch (RemoteException e) { 1523 Log.i(TAG, "Remote error getting media item: " + mediaId); 1524 mHandler.post(new Runnable() { 1525 @Override 1526 public void run() { 1527 cb.onError(mediaId); 1528 } 1529 }); 1530 } 1531 } 1532 1533 @Override 1534 public void onConnected() { 1535 Bundle extras = MediaBrowserCompatApi21.getExtras(mBrowserObj); 1536 if (extras == null) { 1537 return; 1538 } 1539 IBinder serviceBinder = BundleCompat.getBinder(extras, EXTRA_MESSENGER_BINDER); 1540 if (serviceBinder != null) { 1541 mServiceBinderWrapper = new ServiceBinderWrapper(serviceBinder, mRootHints); 1542 mCallbacksMessenger = new Messenger(mHandler); 1543 mHandler.setCallbacksMessenger(mCallbacksMessenger); 1544 try { 1545 mServiceBinderWrapper.registerCallbackMessenger(mCallbacksMessenger); 1546 } catch (RemoteException e) { 1547 Log.i(TAG, "Remote error registering client messenger." ); 1548 } 1549 } 1550 } 1551 1552 @Override 1553 public void onConnectionSuspended() { 1554 mServiceBinderWrapper = null; 1555 mCallbacksMessenger = null; 1556 mHandler.setCallbacksMessenger(null); 1557 } 1558 1559 @Override 1560 public void onConnectionFailed() { 1561 // Do noting 1562 } 1563 1564 @Override 1565 public void onServiceConnected(final Messenger callback, final String root, 1566 final MediaSessionCompat.Token session, final Bundle extra) { 1567 // This method will not be called. 1568 } 1569 1570 @Override 1571 public void onConnectionFailed(Messenger callback) { 1572 // This method will not be called. 1573 } 1574 1575 @Override 1576 public void onLoadChildren(Messenger callback, String parentId, List list, Bundle options) { 1577 if (mCallbacksMessenger != callback) { 1578 return; 1579 } 1580 1581 // Check that the subscription is still subscribed. 1582 Subscription subscription = mSubscriptions.get(parentId); 1583 if (subscription == null) { 1584 if (DEBUG) { 1585 Log.d(TAG, "onLoadChildren for id that isn't subscribed id=" + parentId); 1586 } 1587 return; 1588 } 1589 1590 // Tell the app. 1591 SubscriptionCallback subscriptionCallback = subscription.getCallback(options); 1592 if (subscriptionCallback != null) { 1593 if (options == null) { 1594 if (list == null) { 1595 subscriptionCallback.onError(parentId); 1596 } else { 1597 subscriptionCallback.onChildrenLoaded(parentId, list); 1598 } 1599 } else { 1600 if (list == null) { 1601 subscriptionCallback.onError(parentId, options); 1602 } else { 1603 subscriptionCallback.onChildrenLoaded(parentId, list, options); 1604 } 1605 } 1606 } 1607 } 1608 } 1609 1610 static class MediaBrowserImplApi23 extends MediaBrowserImplApi21 { 1611 public MediaBrowserImplApi23(Context context, ComponentName serviceComponent, 1612 ConnectionCallback callback, Bundle rootHints) { 1613 super(context, serviceComponent, callback, rootHints); 1614 } 1615 1616 @Override 1617 public void getItem(@NonNull final String mediaId, @NonNull final ItemCallback cb) { 1618 if (mServiceBinderWrapper == null) { 1619 MediaBrowserCompatApi23.getItem(mBrowserObj, mediaId, cb.mItemCallbackObj); 1620 } else { 1621 super.getItem(mediaId, cb); 1622 } 1623 } 1624 } 1625 1626 static class MediaBrowserImplApi24 extends MediaBrowserImplApi23 { 1627 public MediaBrowserImplApi24(Context context, ComponentName serviceComponent, 1628 ConnectionCallback callback, Bundle rootHints) { 1629 super(context, serviceComponent, callback, rootHints); 1630 } 1631 1632 @Override 1633 public void subscribe(@NonNull String parentId, @NonNull Bundle options, 1634 @NonNull SubscriptionCallback callback) { 1635 if (options == null) { 1636 MediaBrowserCompatApi21.subscribe( 1637 mBrowserObj, parentId, callback.mSubscriptionCallbackObj); 1638 } else { 1639 MediaBrowserCompatApi24.subscribe( 1640 mBrowserObj, parentId, options, callback.mSubscriptionCallbackObj); 1641 } 1642 } 1643 1644 @Override 1645 public void unsubscribe(@NonNull String parentId, SubscriptionCallback callback) { 1646 if (callback == null) { 1647 MediaBrowserCompatApi21.unsubscribe(mBrowserObj, parentId); 1648 } else { 1649 MediaBrowserCompatApi24.unsubscribe(mBrowserObj, parentId, 1650 callback.mSubscriptionCallbackObj); 1651 } 1652 } 1653 } 1654 1655 private static class Subscription { 1656 private final List<SubscriptionCallback> mCallbacks; 1657 private final List<Bundle> mOptionsList; 1658 1659 public Subscription() { 1660 mCallbacks = new ArrayList(); 1661 mOptionsList = new ArrayList(); 1662 } 1663 1664 public boolean isEmpty() { 1665 return mCallbacks.isEmpty(); 1666 } 1667 1668 public List<Bundle> getOptionsList() { 1669 return mOptionsList; 1670 } 1671 1672 public List<SubscriptionCallback> getCallbacks() { 1673 return mCallbacks; 1674 } 1675 1676 public SubscriptionCallback getCallback(Bundle options) { 1677 for (int i = 0; i < mOptionsList.size(); ++i) { 1678 if (MediaBrowserCompatUtils.areSameOptions(mOptionsList.get(i), options)) { 1679 return mCallbacks.get(i); 1680 } 1681 } 1682 return null; 1683 } 1684 1685 public void putCallback(Bundle options, SubscriptionCallback callback) { 1686 for (int i = 0; i < mOptionsList.size(); ++i) { 1687 if (MediaBrowserCompatUtils.areSameOptions(mOptionsList.get(i), options)) { 1688 mCallbacks.set(i, callback); 1689 return; 1690 } 1691 } 1692 mCallbacks.add(callback); 1693 mOptionsList.add(options); 1694 } 1695 } 1696 1697 private static class CallbackHandler extends Handler { 1698 private final WeakReference<MediaBrowserServiceCallbackImpl> mCallbackImplRef; 1699 private WeakReference<Messenger> mCallbacksMessengerRef; 1700 1701 CallbackHandler(MediaBrowserServiceCallbackImpl callbackImpl) { 1702 super(); 1703 mCallbackImplRef = new WeakReference<>(callbackImpl); 1704 } 1705 1706 @Override 1707 public void handleMessage(Message msg) { 1708 if (mCallbacksMessengerRef == null || mCallbacksMessengerRef.get() == null || 1709 mCallbackImplRef.get() == null) { 1710 return; 1711 } 1712 Bundle data = msg.getData(); 1713 data.setClassLoader(MediaSessionCompat.class.getClassLoader()); 1714 switch (msg.what) { 1715 case SERVICE_MSG_ON_CONNECT: 1716 mCallbackImplRef.get().onServiceConnected(mCallbacksMessengerRef.get(), 1717 data.getString(DATA_MEDIA_ITEM_ID), 1718 (MediaSessionCompat.Token) data.getParcelable(DATA_MEDIA_SESSION_TOKEN), 1719 data.getBundle(DATA_ROOT_HINTS)); 1720 break; 1721 case SERVICE_MSG_ON_CONNECT_FAILED: 1722 mCallbackImplRef.get().onConnectionFailed(mCallbacksMessengerRef.get()); 1723 break; 1724 case SERVICE_MSG_ON_LOAD_CHILDREN: 1725 mCallbackImplRef.get().onLoadChildren(mCallbacksMessengerRef.get(), 1726 data.getString(DATA_MEDIA_ITEM_ID), 1727 data.getParcelableArrayList(DATA_MEDIA_ITEM_LIST), 1728 data.getBundle(DATA_OPTIONS)); 1729 break; 1730 default: 1731 Log.w(TAG, "Unhandled message: " + msg 1732 + "\n Client version: " + CLIENT_VERSION_CURRENT 1733 + "\n Service version: " + msg.arg1); 1734 } 1735 } 1736 1737 void setCallbacksMessenger(Messenger callbacksMessenger) { 1738 mCallbacksMessengerRef = new WeakReference<>(callbacksMessenger); 1739 } 1740 } 1741 1742 private static class ServiceBinderWrapper { 1743 private Messenger mMessenger; 1744 private Bundle mRootHints; 1745 1746 public ServiceBinderWrapper(IBinder target, Bundle rootHints) { 1747 mMessenger = new Messenger(target); 1748 mRootHints = rootHints; 1749 } 1750 1751 void connect(Context context, Messenger callbacksMessenger) 1752 throws RemoteException { 1753 Bundle data = new Bundle(); 1754 data.putString(DATA_PACKAGE_NAME, context.getPackageName()); 1755 data.putBundle(DATA_ROOT_HINTS, mRootHints); 1756 sendRequest(CLIENT_MSG_CONNECT, data, callbacksMessenger); 1757 } 1758 1759 void disconnect(Messenger callbacksMessenger) throws RemoteException { 1760 sendRequest(CLIENT_MSG_DISCONNECT, null, callbacksMessenger); 1761 } 1762 1763 void addSubscription(String parentId, IBinder callbackToken, Bundle options, 1764 Messenger callbacksMessenger) 1765 throws RemoteException { 1766 Bundle data = new Bundle(); 1767 data.putString(DATA_MEDIA_ITEM_ID, parentId); 1768 BundleCompat.putBinder(data, DATA_CALLBACK_TOKEN, callbackToken); 1769 data.putBundle(DATA_OPTIONS, options); 1770 sendRequest(CLIENT_MSG_ADD_SUBSCRIPTION, data, callbacksMessenger); 1771 } 1772 1773 void removeSubscription(String parentId, IBinder callbackToken, 1774 Messenger callbacksMessenger) 1775 throws RemoteException { 1776 Bundle data = new Bundle(); 1777 data.putString(DATA_MEDIA_ITEM_ID, parentId); 1778 BundleCompat.putBinder(data, DATA_CALLBACK_TOKEN, callbackToken); 1779 sendRequest(CLIENT_MSG_REMOVE_SUBSCRIPTION, data, callbacksMessenger); 1780 } 1781 1782 void getMediaItem(String mediaId, ResultReceiver receiver, Messenger callbacksMessenger) 1783 throws RemoteException { 1784 Bundle data = new Bundle(); 1785 data.putString(DATA_MEDIA_ITEM_ID, mediaId); 1786 data.putParcelable(DATA_RESULT_RECEIVER, receiver); 1787 sendRequest(CLIENT_MSG_GET_MEDIA_ITEM, data, callbacksMessenger); 1788 } 1789 1790 void registerCallbackMessenger(Messenger callbackMessenger) throws RemoteException { 1791 Bundle data = new Bundle(); 1792 data.putBundle(DATA_ROOT_HINTS, mRootHints); 1793 sendRequest(CLIENT_MSG_REGISTER_CALLBACK_MESSENGER, data, callbackMessenger); 1794 } 1795 1796 void unregisterCallbackMessenger(Messenger callbackMessenger) throws RemoteException { 1797 sendRequest(CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER, null, callbackMessenger); 1798 } 1799 1800 private void sendRequest(int what, Bundle data, Messenger cbMessenger) 1801 throws RemoteException { 1802 Message msg = Message.obtain(); 1803 msg.what = what; 1804 msg.arg1 = CLIENT_VERSION_CURRENT; 1805 msg.setData(data); 1806 msg.replyTo = cbMessenger; 1807 mMessenger.send(msg); 1808 } 1809 } 1810 1811 private static class ItemReceiver extends ResultReceiver { 1812 private final String mMediaId; 1813 private final ItemCallback mCallback; 1814 1815 ItemReceiver(String mediaId, ItemCallback callback, Handler handler) { 1816 super(handler); 1817 mMediaId = mediaId; 1818 mCallback = callback; 1819 } 1820 1821 @Override 1822 protected void onReceiveResult(int resultCode, Bundle resultData) { 1823 if (resultData != null) { 1824 resultData.setClassLoader(MediaBrowserCompat.class.getClassLoader()); 1825 } 1826 if (resultCode != 0 || resultData == null 1827 || !resultData.containsKey(MediaBrowserServiceCompat.KEY_MEDIA_ITEM)) { 1828 mCallback.onError(mMediaId); 1829 return; 1830 } 1831 Parcelable item = resultData.getParcelable(MediaBrowserServiceCompat.KEY_MEDIA_ITEM); 1832 if (item == null || item instanceof MediaItem) { 1833 mCallback.onItemLoaded((MediaItem) item); 1834 } else { 1835 mCallback.onError(mMediaId); 1836 } 1837 } 1838 } 1839} 1840