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