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