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