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