MediaBrowser.java revision 51af2f017f704f4d2ca7b5f14fd65ac09d85921a
1/* 2 * Copyright (C) 2014 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 17package android.media.browse; 18 19import android.annotation.IntDef; 20import android.annotation.NonNull; 21import android.annotation.Nullable; 22import android.content.ComponentName; 23import android.content.Context; 24import android.content.Intent; 25import android.content.ServiceConnection; 26import android.content.pm.ParceledListSlice; 27import android.media.MediaDescription; 28import android.media.session.MediaController; 29import android.media.session.MediaSession; 30import android.os.Bundle; 31import android.os.Handler; 32import android.os.IBinder; 33import android.os.Parcel; 34import android.os.Parcelable; 35import android.os.RemoteException; 36import android.os.ResultReceiver; 37import android.service.media.IMediaBrowserService; 38import android.service.media.IMediaBrowserServiceCallbacks; 39import android.service.media.MediaBrowserService; 40import android.text.TextUtils; 41import android.util.ArrayMap; 42import android.util.Log; 43 44import java.lang.annotation.Retention; 45import java.lang.annotation.RetentionPolicy; 46import java.lang.ref.WeakReference; 47import java.util.ArrayList; 48import java.util.List; 49import java.util.Map.Entry; 50 51/** 52 * Browses media content offered by a link MediaBrowserService. 53 * <p> 54 * This object is not thread-safe. All calls should happen on the thread on which the browser 55 * was constructed. 56 * </p> 57 * <h3>Standard Extra Data</h3> 58 * 59 * <p>These are the current standard fields that can be used as extra data via 60 * {@link #subscribe(String, Bundle, SubscriptionCallback)}, {@link #unsubscribe(String, Bundle)}, 61 * and {@link SubscriptionCallback#onChildrenLoaded(String, List, Bundle)}. 62 * 63 * <ul> 64 * <li> {@link #EXTRA_PAGE} 65 * <li> {@link #EXTRA_PAGE_SIZE} 66 * </ul> 67 */ 68public final class MediaBrowser { 69 private static final String TAG = "MediaBrowser"; 70 private static final boolean DBG = false; 71 72 /** 73 * Used as an int extra field to denote the page number to subscribe. 74 * The value of {@code EXTRA_PAGE} should be greater than or equal to 1. 75 * 76 * @see android.service.media.MediaBrowserService.BrowserRoot 77 * @see #EXTRA_PAGE_SIZE 78 */ 79 public static final String EXTRA_PAGE = "android.media.browse.extra.PAGE"; 80 81 /** 82 * Used as an int extra field to denote the number of media items in a page. 83 * The value of {@code EXTRA_PAGE_SIZE} should be greater than or equal to 1. 84 * 85 * @see android.service.media.MediaBrowserService.BrowserRoot 86 * @see #EXTRA_PAGE 87 */ 88 public static final String EXTRA_PAGE_SIZE = "android.media.browse.extra.PAGE_SIZE"; 89 90 private static final int CONNECT_STATE_DISCONNECTED = 0; 91 private static final int CONNECT_STATE_CONNECTING = 1; 92 private static final int CONNECT_STATE_CONNECTED = 2; 93 private static final int CONNECT_STATE_SUSPENDED = 3; 94 95 private final Context mContext; 96 private final ComponentName mServiceComponent; 97 private final ConnectionCallback mCallback; 98 private final Bundle mRootHints; 99 private final Handler mHandler = new Handler(); 100 private final ArrayMap<String, Subscription> mSubscriptions = new ArrayMap<>(); 101 102 private volatile int mState = CONNECT_STATE_DISCONNECTED; 103 private volatile String mRootId; 104 private volatile MediaSession.Token mMediaSessionToken; 105 private volatile Bundle mExtras; 106 107 private MediaServiceConnection mServiceConnection; 108 private IMediaBrowserService mServiceBinder; 109 private IMediaBrowserServiceCallbacks mServiceCallbacks; 110 111 /** 112 * Creates a media browser for the specified media browse service. 113 * 114 * @param context The context. 115 * @param serviceComponent The component name of the media browse service. 116 * @param callback The connection callback. 117 * @param rootHints An optional bundle of service-specific arguments to send 118 * to the media browse service when connecting and retrieving the root id 119 * for browsing, or null if none. The contents of this bundle may affect 120 * the information returned when browsing. 121 * @see android.service.media.MediaBrowserService.BrowserRoot#EXTRA_RECENT 122 * @see android.service.media.MediaBrowserService.BrowserRoot#EXTRA_OFFLINE 123 * @see android.service.media.MediaBrowserService.BrowserRoot#EXTRA_SUGGESTED 124 */ 125 public MediaBrowser(Context context, ComponentName serviceComponent, 126 ConnectionCallback callback, Bundle rootHints) { 127 if (context == null) { 128 throw new IllegalArgumentException("context must not be null"); 129 } 130 if (serviceComponent == null) { 131 throw new IllegalArgumentException("service component must not be null"); 132 } 133 if (callback == null) { 134 throw new IllegalArgumentException("connection callback must not be null"); 135 } 136 mContext = context; 137 mServiceComponent = serviceComponent; 138 mCallback = callback; 139 mRootHints = rootHints; 140 } 141 142 /** 143 * Connects to the media browse service. 144 * <p> 145 * The connection callback specified in the constructor will be invoked 146 * when the connection completes or fails. 147 * </p> 148 */ 149 public void connect() { 150 if (mState != CONNECT_STATE_DISCONNECTED) { 151 throw new IllegalStateException("connect() called while not disconnected (state=" 152 + getStateLabel(mState) + ")"); 153 } 154 // TODO: remove this extra check. 155 if (DBG) { 156 if (mServiceConnection != null) { 157 throw new RuntimeException("mServiceConnection should be null. Instead it is " 158 + mServiceConnection); 159 } 160 } 161 if (mServiceBinder != null) { 162 throw new RuntimeException("mServiceBinder should be null. Instead it is " 163 + mServiceBinder); 164 } 165 if (mServiceCallbacks != null) { 166 throw new RuntimeException("mServiceCallbacks should be null. Instead it is " 167 + mServiceCallbacks); 168 } 169 170 mState = CONNECT_STATE_CONNECTING; 171 172 final Intent intent = new Intent(MediaBrowserService.SERVICE_INTERFACE); 173 intent.setComponent(mServiceComponent); 174 175 final ServiceConnection thisConnection = mServiceConnection = new MediaServiceConnection(); 176 177 boolean bound = false; 178 try { 179 bound = mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE); 180 } catch (Exception ex) { 181 Log.e(TAG, "Failed binding to service " + mServiceComponent); 182 } 183 184 if (!bound) { 185 // Tell them that it didn't work. We are already on the main thread, 186 // but we don't want to do callbacks inside of connect(). So post it, 187 // and then check that we are on the same ServiceConnection. We know 188 // we won't also get an onServiceConnected or onServiceDisconnected, 189 // so we won't be doing double callbacks. 190 mHandler.post(new Runnable() { 191 @Override 192 public void run() { 193 // Ensure that nobody else came in or tried to connect again. 194 if (thisConnection == mServiceConnection) { 195 forceCloseConnection(); 196 mCallback.onConnectionFailed(); 197 } 198 } 199 }); 200 } 201 202 if (DBG) { 203 Log.d(TAG, "connect..."); 204 dump(); 205 } 206 } 207 208 /** 209 * Disconnects from the media browse service. 210 * After this, no more callbacks will be received. 211 */ 212 public void disconnect() { 213 // It's ok to call this any state, because allowing this lets apps not have 214 // to check isConnected() unnecessarily. They won't appreciate the extra 215 // assertions for this. We do everything we can here to go back to a sane state. 216 if (mServiceCallbacks != null) { 217 try { 218 mServiceBinder.disconnect(mServiceCallbacks); 219 } catch (RemoteException ex) { 220 // We are disconnecting anyway. Log, just for posterity but it's not 221 // a big problem. 222 Log.w(TAG, "RemoteException during connect for " + mServiceComponent); 223 } 224 } 225 forceCloseConnection(); 226 227 if (DBG) { 228 Log.d(TAG, "disconnect..."); 229 dump(); 230 } 231 } 232 233 /** 234 * Null out the variables and unbind from the service. This doesn't include 235 * calling disconnect on the service, because we only try to do that in the 236 * clean shutdown cases. 237 * <p> 238 * Everywhere that calls this EXCEPT for disconnect() should follow it with 239 * a call to mCallback.onConnectionFailed(). Disconnect doesn't do that callback 240 * for a clean shutdown, but everywhere else is a dirty shutdown and should 241 * notify the app. 242 */ 243 private void forceCloseConnection() { 244 if (mServiceConnection != null) { 245 mContext.unbindService(mServiceConnection); 246 } 247 mState = CONNECT_STATE_DISCONNECTED; 248 mServiceConnection = null; 249 mServiceBinder = null; 250 mServiceCallbacks = null; 251 mRootId = null; 252 mMediaSessionToken = null; 253 } 254 255 /** 256 * Returns whether the browser is connected to the service. 257 */ 258 public boolean isConnected() { 259 return mState == CONNECT_STATE_CONNECTED; 260 } 261 262 /** 263 * Gets the service component that the media browser is connected to. 264 */ 265 public @NonNull ComponentName getServiceComponent() { 266 if (!isConnected()) { 267 throw new IllegalStateException("getServiceComponent() called while not connected" + 268 " (state=" + mState + ")"); 269 } 270 return mServiceComponent; 271 } 272 273 /** 274 * Gets the root id. 275 * <p> 276 * Note that the root id may become invalid or change when the 277 * browser is disconnected. 278 * </p> 279 * 280 * @throws IllegalStateException if not connected. 281 */ 282 public @NonNull String getRoot() { 283 if (!isConnected()) { 284 throw new IllegalStateException("getRoot() called while not connected (state=" 285 + getStateLabel(mState) + ")"); 286 } 287 return mRootId; 288 } 289 290 /** 291 * Gets any extras for the media service. 292 * 293 * @throws IllegalStateException if not connected. 294 */ 295 public @Nullable Bundle getExtras() { 296 if (!isConnected()) { 297 throw new IllegalStateException("getExtras() called while not connected (state=" 298 + getStateLabel(mState) + ")"); 299 } 300 return mExtras; 301 } 302 303 /** 304 * Gets the media session token associated with the media browser. 305 * <p> 306 * Note that the session token may become invalid or change when the 307 * browser is disconnected. 308 * </p> 309 * 310 * @return The session token for the browser, never null. 311 * 312 * @throws IllegalStateException if not connected. 313 */ 314 public @NonNull MediaSession.Token getSessionToken() { 315 if (!isConnected()) { 316 throw new IllegalStateException("getSessionToken() called while not connected (state=" 317 + mState + ")"); 318 } 319 return mMediaSessionToken; 320 } 321 322 /** 323 * Queries for information about the media items that are contained within 324 * the specified id and subscribes to receive updates when they change. 325 * <p> 326 * The list of subscriptions is maintained even when not connected and is 327 * restored after the reconnection. It is ok to subscribe while not connected 328 * but the results will not be returned until the connection completes. 329 * </p> 330 * <p> 331 * If the id is already subscribed with a different callback then the new 332 * callback will replace the previous one and the child data will be 333 * reloaded. 334 * </p> 335 * 336 * @param parentId The id of the parent media item whose list of children 337 * will be subscribed. 338 * @param callback The callback to receive the list of children. 339 */ 340 public void subscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) { 341 subscribeInternal(parentId, null, callback); 342 } 343 344 /** 345 * Queries with service-specific arguments for information about the media items 346 * that are contained within the specified id and subscribes to receive updates 347 * when they change. 348 * <p> 349 * The list of subscriptions is maintained even when not connected and is 350 * restored after the reconnection. It is ok to subscribe while not connected 351 * but the results will not be returned until the connection completes. 352 * </p> 353 * <p> 354 * If the id is already subscribed with a different callback then the new 355 * callback will replace the previous one and the child data will be 356 * reloaded. 357 * </p> 358 * 359 * @param parentId The id of the parent media item whose list of children 360 * will be subscribed. 361 * @param options A bundle of service-specific arguments to send to the media 362 * browse service. The contents of this bundle may affect the 363 * information returned when browsing. 364 * @param callback The callback to receive the list of children. 365 */ 366 public void subscribe(@NonNull String parentId, @NonNull Bundle options, 367 @NonNull SubscriptionCallback callback) { 368 if (options == null) { 369 throw new IllegalArgumentException("options are null"); 370 } 371 subscribeInternal(parentId, options, callback); 372 } 373 374 /** 375 * Unsubscribes for changes to the children of the specified media id. 376 * <p> 377 * The query callback will no longer be invoked for results associated with 378 * this id once this method returns. 379 * </p> 380 * 381 * @param parentId The id of the parent media item whose list of children 382 * will be unsubscribed. 383 */ 384 public void unsubscribe(@NonNull String parentId) { 385 unsubscribeInternal(parentId, null); 386 } 387 388 /** 389 * Unsubscribes for changes to the children of the specified media id. 390 * <p> 391 * The query callback will no longer be invoked for results associated with 392 * this id once this method returns. 393 * </p> 394 * 395 * @param parentId The id of the parent media item whose list of children 396 * will be unsubscribed. 397 * @param options A bundle sent to the media browse service to subscribe. 398 */ 399 public void unsubscribe(@NonNull String parentId, @NonNull Bundle options) { 400 if (options == null) { 401 throw new IllegalArgumentException("options are null"); 402 } 403 unsubscribeInternal(parentId, options); 404 } 405 406 /** 407 * Retrieves a specific {@link MediaItem} from the connected service. Not 408 * all services may support this, so falling back to subscribing to the 409 * parent's id should be used when unavailable. 410 * 411 * @param mediaId The id of the item to retrieve. 412 * @param cb The callback to receive the result on. 413 */ 414 public void getItem(final @NonNull String mediaId, @NonNull final ItemCallback cb) { 415 if (TextUtils.isEmpty(mediaId)) { 416 throw new IllegalArgumentException("mediaId is empty."); 417 } 418 if (cb == null) { 419 throw new IllegalArgumentException("cb is null."); 420 } 421 if (mState != CONNECT_STATE_CONNECTED) { 422 Log.i(TAG, "Not connected, unable to retrieve the MediaItem."); 423 mHandler.post(new Runnable() { 424 @Override 425 public void run() { 426 cb.onError(mediaId); 427 } 428 }); 429 return; 430 } 431 ResultReceiver receiver = new ResultReceiver(mHandler) { 432 @Override 433 protected void onReceiveResult(int resultCode, Bundle resultData) { 434 if (resultCode != 0 || resultData == null 435 || !resultData.containsKey(MediaBrowserService.KEY_MEDIA_ITEM)) { 436 cb.onError(mediaId); 437 return; 438 } 439 Parcelable item = resultData.getParcelable(MediaBrowserService.KEY_MEDIA_ITEM); 440 if (!(item instanceof MediaItem)) { 441 cb.onError(mediaId); 442 return; 443 } 444 cb.onItemLoaded((MediaItem)item); 445 } 446 }; 447 try { 448 mServiceBinder.getMediaItem(mediaId, receiver); 449 } catch (RemoteException e) { 450 Log.i(TAG, "Remote error getting media item."); 451 mHandler.post(new Runnable() { 452 @Override 453 public void run() { 454 cb.onError(mediaId); 455 } 456 }); 457 } 458 } 459 460 private void subscribeInternal(String parentId, Bundle options, SubscriptionCallback callback) { 461 // Check arguments. 462 if (TextUtils.isEmpty(parentId)) { 463 throw new IllegalArgumentException("parentId is empty."); 464 } 465 if (callback == null) { 466 throw new IllegalArgumentException("callback is null"); 467 } 468 // Update or create the subscription. 469 Subscription sub = mSubscriptions.get(parentId); 470 if (sub == null) { 471 sub = new Subscription(); 472 mSubscriptions.put(parentId, sub); 473 } 474 sub.add(callback, options); 475 476 // If we are connected, tell the service that we are watching. If we aren't connected, 477 // the service will be told when we connect. 478 if (mState == CONNECT_STATE_CONNECTED) { 479 try { 480 // NOTE: Do not call addSubscriptionWithOptions when options are null. Otherwise, 481 // it will break the action of support library which expects addSubscription will 482 // be called when options are null. 483 if (options == null) { 484 mServiceBinder.addSubscription(parentId, mServiceCallbacks); 485 } else { 486 mServiceBinder.addSubscriptionWithOptions(parentId, options, mServiceCallbacks); 487 } 488 } catch (RemoteException ex) { 489 // Process is crashing. We will disconnect, and upon reconnect we will 490 // automatically reregister. So nothing to do here. 491 Log.d(TAG, "addSubscription failed with RemoteException parentId=" + parentId); 492 } 493 } 494 } 495 496 private void unsubscribeInternal(String parentId, Bundle options) { 497 // Check arguments. 498 if (TextUtils.isEmpty(parentId)) { 499 throw new IllegalArgumentException("parentId is empty."); 500 } 501 502 // Remove from our list. 503 Subscription sub = mSubscriptions.get(parentId); 504 505 // Tell the service if necessary. 506 if (sub != null && sub.remove(options) && mState == CONNECT_STATE_CONNECTED) { 507 try { 508 // NOTE: Do not call removeSubscriptionWithOptions when options are null. Otherwise, 509 // it will break the action of support library which expects removeSubscription will 510 // be called when options are null. 511 if (options == null) { 512 mServiceBinder.removeSubscription(parentId, mServiceCallbacks); 513 } else { 514 mServiceBinder.removeSubscriptionWithOptions( 515 parentId, options, mServiceCallbacks); 516 } 517 } catch (RemoteException ex) { 518 // Process is crashing. We will disconnect, and upon reconnect we will 519 // automatically reregister. So nothing to do here. 520 Log.d(TAG, "removeSubscription failed with RemoteException parentId=" + parentId); 521 } 522 } 523 if (sub != null && sub.isEmpty()) { 524 mSubscriptions.remove(parentId); 525 } 526 } 527 528 /** 529 * For debugging. 530 */ 531 private static String getStateLabel(int state) { 532 switch (state) { 533 case CONNECT_STATE_DISCONNECTED: 534 return "CONNECT_STATE_DISCONNECTED"; 535 case CONNECT_STATE_CONNECTING: 536 return "CONNECT_STATE_CONNECTING"; 537 case CONNECT_STATE_CONNECTED: 538 return "CONNECT_STATE_CONNECTED"; 539 case CONNECT_STATE_SUSPENDED: 540 return "CONNECT_STATE_SUSPENDED"; 541 default: 542 return "UNKNOWN/" + state; 543 } 544 } 545 546 private final void onServiceConnected(final IMediaBrowserServiceCallbacks callback, 547 final String root, final MediaSession.Token session, final Bundle extra) { 548 mHandler.post(new Runnable() { 549 @Override 550 public void run() { 551 // Check to make sure there hasn't been a disconnect or a different 552 // ServiceConnection. 553 if (!isCurrent(callback, "onConnect")) { 554 return; 555 } 556 // Don't allow them to call us twice. 557 if (mState != CONNECT_STATE_CONNECTING) { 558 Log.w(TAG, "onConnect from service while mState=" 559 + getStateLabel(mState) + "... ignoring"); 560 return; 561 } 562 mRootId = root; 563 mMediaSessionToken = session; 564 mExtras = extra; 565 mState = CONNECT_STATE_CONNECTED; 566 567 if (DBG) { 568 Log.d(TAG, "ServiceCallbacks.onConnect..."); 569 dump(); 570 } 571 mCallback.onConnected(); 572 573 // we may receive some subscriptions before we are connected, so re-subscribe 574 // everything now 575 for (Entry<String, Subscription> subscriptionEntry : mSubscriptions.entrySet()) { 576 String id = subscriptionEntry.getKey(); 577 Subscription sub = subscriptionEntry.getValue(); 578 for (Bundle options : sub.getOptionsList()) { 579 try { 580 // NOTE: Do not call addSubscriptionWithOptions when options are null. 581 // Otherwise, it will break the action of support library which expects 582 // addSubscription will be called when options are null. 583 if (options == null) { 584 mServiceBinder.addSubscription(id, mServiceCallbacks); 585 } else { 586 mServiceBinder.addSubscriptionWithOptions( 587 id, options, mServiceCallbacks); 588 } 589 } catch (RemoteException ex) { 590 // Process is crashing. We will disconnect, and upon reconnect we will 591 // automatically reregister. So nothing to do here. 592 Log.d(TAG, "addSubscription failed with RemoteException parentId=" 593 + id); 594 } 595 } 596 } 597 } 598 }); 599 } 600 601 private final void onConnectionFailed(final IMediaBrowserServiceCallbacks callback) { 602 mHandler.post(new Runnable() { 603 @Override 604 public void run() { 605 Log.e(TAG, "onConnectFailed for " + mServiceComponent); 606 607 // Check to make sure there hasn't been a disconnect or a different 608 // ServiceConnection. 609 if (!isCurrent(callback, "onConnectFailed")) { 610 return; 611 } 612 // Don't allow them to call us twice. 613 if (mState != CONNECT_STATE_CONNECTING) { 614 Log.w(TAG, "onConnect from service while mState=" 615 + getStateLabel(mState) + "... ignoring"); 616 return; 617 } 618 619 // Clean up 620 forceCloseConnection(); 621 622 // Tell the app. 623 mCallback.onConnectionFailed(); 624 } 625 }); 626 } 627 628 private final void onLoadChildren(final IMediaBrowserServiceCallbacks callback, 629 final String parentId, final ParceledListSlice list, final Bundle options) { 630 mHandler.post(new Runnable() { 631 @Override 632 public void run() { 633 // Check that there hasn't been a disconnect or a different 634 // ServiceConnection. 635 if (!isCurrent(callback, "onLoadChildren")) { 636 return; 637 } 638 639 List<MediaItem> data = list == null ? null : list.getList(); 640 if (DBG) { 641 Log.d(TAG, "onLoadChildren for " + mServiceComponent + " id=" + parentId); 642 } 643 644 // Check that the subscription is still subscribed. 645 final Subscription subscription = mSubscriptions.get(parentId); 646 if (subscription != null) { 647 // Tell the app. 648 SubscriptionCallback subscriptionCallback = subscription.getCallback(options); 649 if (subscriptionCallback != null) { 650 if (options == null) { 651 subscriptionCallback.onChildrenLoaded(parentId, data); 652 } else { 653 subscriptionCallback.onChildrenLoaded(parentId, data, options); 654 } 655 return; 656 } 657 } 658 if (DBG) { 659 Log.d(TAG, "onLoadChildren for id that isn't subscribed id=" + parentId); 660 } 661 } 662 }); 663 } 664 665 /** 666 * Return true if {@code callback} is the current ServiceCallbacks. Also logs if it's not. 667 */ 668 private boolean isCurrent(IMediaBrowserServiceCallbacks callback, String funcName) { 669 if (mServiceCallbacks != callback) { 670 if (mState != CONNECT_STATE_DISCONNECTED) { 671 Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection=" 672 + mServiceCallbacks + " this=" + this); 673 } 674 return false; 675 } 676 return true; 677 } 678 679 private ServiceCallbacks getNewServiceCallbacks() { 680 return new ServiceCallbacks(this); 681 } 682 683 /** 684 * Log internal state. 685 * @hide 686 */ 687 void dump() { 688 Log.d(TAG, "MediaBrowser..."); 689 Log.d(TAG, " mServiceComponent=" + mServiceComponent); 690 Log.d(TAG, " mCallback=" + mCallback); 691 Log.d(TAG, " mRootHints=" + mRootHints); 692 Log.d(TAG, " mState=" + getStateLabel(mState)); 693 Log.d(TAG, " mServiceConnection=" + mServiceConnection); 694 Log.d(TAG, " mServiceBinder=" + mServiceBinder); 695 Log.d(TAG, " mServiceCallbacks=" + mServiceCallbacks); 696 Log.d(TAG, " mRootId=" + mRootId); 697 Log.d(TAG, " mMediaSessionToken=" + mMediaSessionToken); 698 } 699 700 /** 701 * A class with information on a single media item for use in browsing media. 702 */ 703 public static class MediaItem implements Parcelable { 704 private final int mFlags; 705 private final MediaDescription mDescription; 706 707 /** @hide */ 708 @Retention(RetentionPolicy.SOURCE) 709 @IntDef(flag=true, value = { FLAG_BROWSABLE, FLAG_PLAYABLE }) 710 public @interface Flags { } 711 712 /** 713 * Flag: Indicates that the item has children of its own. 714 */ 715 public static final int FLAG_BROWSABLE = 1 << 0; 716 717 /** 718 * Flag: Indicates that the item is playable. 719 * <p> 720 * The id of this item may be passed to 721 * {@link MediaController.TransportControls#playFromMediaId(String, Bundle)} 722 * to start playing it. 723 * </p> 724 */ 725 public static final int FLAG_PLAYABLE = 1 << 1; 726 727 /** 728 * Create a new MediaItem for use in browsing media. 729 * @param description The description of the media, which must include a 730 * media id. 731 * @param flags The flags for this item. 732 */ 733 public MediaItem(@NonNull MediaDescription description, @Flags int flags) { 734 if (description == null) { 735 throw new IllegalArgumentException("description cannot be null"); 736 } 737 if (TextUtils.isEmpty(description.getMediaId())) { 738 throw new IllegalArgumentException("description must have a non-empty media id"); 739 } 740 mFlags = flags; 741 mDescription = description; 742 } 743 744 /** 745 * Private constructor. 746 */ 747 private MediaItem(Parcel in) { 748 mFlags = in.readInt(); 749 mDescription = MediaDescription.CREATOR.createFromParcel(in); 750 } 751 752 @Override 753 public int describeContents() { 754 return 0; 755 } 756 757 @Override 758 public void writeToParcel(Parcel out, int flags) { 759 out.writeInt(mFlags); 760 mDescription.writeToParcel(out, flags); 761 } 762 763 @Override 764 public String toString() { 765 final StringBuilder sb = new StringBuilder("MediaItem{"); 766 sb.append("mFlags=").append(mFlags); 767 sb.append(", mDescription=").append(mDescription); 768 sb.append('}'); 769 return sb.toString(); 770 } 771 772 public static final Parcelable.Creator<MediaItem> CREATOR = 773 new Parcelable.Creator<MediaItem>() { 774 @Override 775 public MediaItem createFromParcel(Parcel in) { 776 return new MediaItem(in); 777 } 778 779 @Override 780 public MediaItem[] newArray(int size) { 781 return new MediaItem[size]; 782 } 783 }; 784 785 /** 786 * Gets the flags of the item. 787 */ 788 public @Flags int getFlags() { 789 return mFlags; 790 } 791 792 /** 793 * Returns whether this item is browsable. 794 * @see #FLAG_BROWSABLE 795 */ 796 public boolean isBrowsable() { 797 return (mFlags & FLAG_BROWSABLE) != 0; 798 } 799 800 /** 801 * Returns whether this item is playable. 802 * @see #FLAG_PLAYABLE 803 */ 804 public boolean isPlayable() { 805 return (mFlags & FLAG_PLAYABLE) != 0; 806 } 807 808 /** 809 * Returns the description of the media. 810 */ 811 public @NonNull MediaDescription getDescription() { 812 return mDescription; 813 } 814 815 /** 816 * Returns the media id for this item. 817 */ 818 public @NonNull String getMediaId() { 819 return mDescription.getMediaId(); 820 } 821 } 822 823 /** 824 * Callbacks for connection related events. 825 */ 826 public static class ConnectionCallback { 827 /** 828 * Invoked after {@link MediaBrowser#connect()} when the request has successfully completed. 829 */ 830 public void onConnected() { 831 } 832 833 /** 834 * Invoked when the client is disconnected from the media browser. 835 */ 836 public void onConnectionSuspended() { 837 } 838 839 /** 840 * Invoked when the connection to the media browser failed. 841 */ 842 public void onConnectionFailed() { 843 } 844 } 845 846 /** 847 * Callbacks for subscription related events. 848 */ 849 public static abstract class SubscriptionCallback { 850 /** 851 * Called when the list of children is loaded or updated. 852 * 853 * @param parentId The media id of the parent media item. 854 * @param children The children which were loaded, or null if the id is invalid. 855 */ 856 public void onChildrenLoaded(@NonNull String parentId, List<MediaItem> children) { 857 } 858 859 /** 860 * Called when the list of children is loaded or updated. 861 * 862 * @param parentId The media id of the parent media item. 863 * @param children The children which were loaded, or null if the id is invalid. 864 * @param options A bundle of service-specific arguments to send to the media 865 * browse service. The contents of this bundle may affect the 866 * information returned when browsing. 867 */ 868 public void onChildrenLoaded(@NonNull String parentId, List<MediaItem> children, 869 @NonNull Bundle options) { 870 } 871 872 /** 873 * Called when the id doesn't exist or other errors in subscribing. 874 * <p> 875 * If this is called, the subscription remains until {@link MediaBrowser#unsubscribe} 876 * called, because some errors may heal themselves. 877 * </p> 878 * 879 * @param parentId The media id of the parent media item whose children could 880 * not be loaded. 881 */ 882 public void onError(@NonNull String parentId) { 883 } 884 885 /** 886 * Called when the id doesn't exist or other errors in subscribing. 887 * <p> 888 * If this is called, the subscription remains until {@link MediaBrowser#unsubscribe} 889 * called, because some errors may heal themselves. 890 * </p> 891 * 892 * @param parentId The media id of the parent media item whose children could 893 * not be loaded. 894 * @param options A bundle of service-specific arguments sent to the media 895 * browse service. 896 */ 897 public void onError(@NonNull String parentId, @NonNull Bundle options) { 898 } 899 } 900 901 /** 902 * Callback for receiving the result of {@link #getItem}. 903 */ 904 public static abstract class ItemCallback { 905 /** 906 * Called when the item has been returned by the browser service. 907 * 908 * @param item The item that was returned or null if it doesn't exist. 909 */ 910 public void onItemLoaded(MediaItem item) { 911 } 912 913 /** 914 * Called when the item doesn't exist or there was an error retrieving it. 915 * 916 * @param itemId The media id of the media item which could not be loaded. 917 */ 918 public void onError(@NonNull String itemId) { 919 } 920 } 921 922 /** 923 * ServiceConnection to the other app. 924 */ 925 private class MediaServiceConnection implements ServiceConnection { 926 @Override 927 public void onServiceConnected(final ComponentName name, final IBinder binder) { 928 postOrRun(new Runnable() { 929 @Override 930 public void run() { 931 if (DBG) { 932 Log.d(TAG, "MediaServiceConnection.onServiceConnected name=" + name 933 + " binder=" + binder); 934 dump(); 935 } 936 937 // Make sure we are still the current connection, and that they haven't called 938 // disconnect(). 939 if (!isCurrent("onServiceConnected")) { 940 return; 941 } 942 943 // Save their binder 944 mServiceBinder = IMediaBrowserService.Stub.asInterface(binder); 945 946 // We make a new mServiceCallbacks each time we connect so that we can drop 947 // responses from previous connections. 948 mServiceCallbacks = getNewServiceCallbacks(); 949 mState = CONNECT_STATE_CONNECTING; 950 951 // Call connect, which is async. When we get a response from that we will 952 // say that we're connected. 953 try { 954 if (DBG) { 955 Log.d(TAG, "ServiceCallbacks.onConnect..."); 956 dump(); 957 } 958 mServiceBinder.connect(mContext.getPackageName(), mRootHints, 959 mServiceCallbacks); 960 } catch (RemoteException ex) { 961 // Connect failed, which isn't good. But the auto-reconnect on the service 962 // will take over and we will come back. We will also get the 963 // onServiceDisconnected, which has all the cleanup code. So let that do 964 // it. 965 Log.w(TAG, "RemoteException during connect for " + mServiceComponent); 966 if (DBG) { 967 Log.d(TAG, "ServiceCallbacks.onConnect..."); 968 dump(); 969 } 970 } 971 } 972 }); 973 } 974 975 @Override 976 public void onServiceDisconnected(final ComponentName name) { 977 postOrRun(new Runnable() { 978 @Override 979 public void run() { 980 if (DBG) { 981 Log.d(TAG, "MediaServiceConnection.onServiceDisconnected name=" + name 982 + " this=" + this + " mServiceConnection=" + mServiceConnection); 983 dump(); 984 } 985 986 // Make sure we are still the current connection, and that they haven't called 987 // disconnect(). 988 if (!isCurrent("onServiceDisconnected")) { 989 return; 990 } 991 992 // Clear out what we set in onServiceConnected 993 mServiceBinder = null; 994 mServiceCallbacks = null; 995 996 // And tell the app that it's suspended. 997 mState = CONNECT_STATE_SUSPENDED; 998 mCallback.onConnectionSuspended(); 999 } 1000 }); 1001 } 1002 1003 private void postOrRun(Runnable r) { 1004 if (Thread.currentThread() == mHandler.getLooper().getThread()) { 1005 r.run(); 1006 } else { 1007 mHandler.post(r); 1008 } 1009 } 1010 1011 /** 1012 * Return true if this is the current ServiceConnection. Also logs if it's not. 1013 */ 1014 private boolean isCurrent(String funcName) { 1015 if (mServiceConnection != this) { 1016 if (mState != CONNECT_STATE_DISCONNECTED) { 1017 // Check mState, because otherwise this log is noisy. 1018 Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection=" 1019 + mServiceConnection + " this=" + this); 1020 } 1021 return false; 1022 } 1023 return true; 1024 } 1025 } 1026 1027 /** 1028 * Callbacks from the service. 1029 */ 1030 private static class ServiceCallbacks extends IMediaBrowserServiceCallbacks.Stub { 1031 private WeakReference<MediaBrowser> mMediaBrowser; 1032 1033 public ServiceCallbacks(MediaBrowser mediaBrowser) { 1034 mMediaBrowser = new WeakReference<MediaBrowser>(mediaBrowser); 1035 } 1036 1037 /** 1038 * The other side has acknowledged our connection. The parameters to this function 1039 * are the initial data as requested. 1040 */ 1041 @Override 1042 public void onConnect(String root, MediaSession.Token session, 1043 final Bundle extras) { 1044 MediaBrowser mediaBrowser = mMediaBrowser.get(); 1045 if (mediaBrowser != null) { 1046 mediaBrowser.onServiceConnected(this, root, session, extras); 1047 } 1048 } 1049 1050 /** 1051 * The other side does not like us. Tell the app via onConnectionFailed. 1052 */ 1053 @Override 1054 public void onConnectFailed() { 1055 MediaBrowser mediaBrowser = mMediaBrowser.get(); 1056 if (mediaBrowser != null) { 1057 mediaBrowser.onConnectionFailed(this); 1058 } 1059 } 1060 1061 @Override 1062 public void onLoadChildren(String parentId, ParceledListSlice list) { 1063 onLoadChildrenWithOptions(parentId, list, null); 1064 } 1065 1066 @Override 1067 public void onLoadChildrenWithOptions(String parentId, ParceledListSlice list, 1068 final Bundle options) { 1069 MediaBrowser mediaBrowser = mMediaBrowser.get(); 1070 if (mediaBrowser != null) { 1071 mediaBrowser.onLoadChildren(this, parentId, list, options); 1072 } 1073 } 1074 } 1075 1076 private static class Subscription { 1077 private final List<SubscriptionCallback> mCallbacks; 1078 private final List<Bundle> mOptionsList; 1079 1080 public Subscription() { 1081 mCallbacks = new ArrayList<>(); 1082 mOptionsList = new ArrayList<>(); 1083 } 1084 1085 public boolean isEmpty() { 1086 return mCallbacks.isEmpty(); 1087 } 1088 1089 public List<Bundle> getOptionsList() { 1090 return mOptionsList; 1091 } 1092 1093 public List<SubscriptionCallback> getCallbacks() { 1094 return mCallbacks; 1095 } 1096 1097 public void add(SubscriptionCallback callback, Bundle options) { 1098 for (int i = 0; i < mOptionsList.size(); ++i) { 1099 if (MediaBrowserUtils.areSameOptions(mOptionsList.get(i), options)) { 1100 mCallbacks.set(i, callback); 1101 return; 1102 } 1103 } 1104 mCallbacks.add(callback); 1105 mOptionsList.add(options); 1106 } 1107 1108 public boolean remove(Bundle options) { 1109 for (int i = 0; i < mOptionsList.size(); ++i) { 1110 if (MediaBrowserUtils.areSameOptions(mOptionsList.get(i), options)) { 1111 mCallbacks.remove(i); 1112 mOptionsList.remove(i); 1113 return true; 1114 } 1115 } 1116 return false; 1117 } 1118 1119 public SubscriptionCallback getCallback(Bundle options) { 1120 for (int i = 0; i < mOptionsList.size(); ++i) { 1121 if (MediaBrowserUtils.areSameOptions(mOptionsList.get(i), options)) { 1122 return mCallbacks.get(i); 1123 } 1124 } 1125 return null; 1126 } 1127 } 1128} 1129