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