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