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