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