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