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