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