MediaBrowser.java revision 17872a85804cfad4b93f8ada5fb379ff952af9f1
1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.media.browse; 18 19import android.annotation.IntDef; 20import android.annotation.NonNull; 21import android.annotation.Nullable; 22import android.content.ComponentName; 23import android.content.Context; 24import android.content.Intent; 25import android.content.ServiceConnection; 26import android.content.pm.ParceledListSlice; 27import android.media.MediaDescription; 28import android.media.session.MediaController; 29import android.media.session.MediaSession; 30import android.os.Bundle; 31import android.os.Handler; 32import android.os.IBinder; 33import android.os.Parcel; 34import android.os.Parcelable; 35import android.os.RemoteException; 36import android.os.ResultReceiver; 37import android.service.media.IMediaBrowserService; 38import android.service.media.IMediaBrowserServiceCallbacks; 39import android.service.media.MediaBrowserService; 40import android.text.TextUtils; 41import android.util.ArrayMap; 42import android.util.Log; 43 44import java.lang.annotation.Retention; 45import java.lang.annotation.RetentionPolicy; 46import java.lang.ref.WeakReference; 47import java.util.ArrayList; 48import java.util.List; 49import java.util.Map.Entry; 50 51/** 52 * Browses media content offered by a link MediaBrowserService. 53 * <p> 54 * This object is not thread-safe. All calls should happen on the thread on which the browser 55 * was constructed. 56 * </p> 57 * <h3>Standard Extra Data</h3> 58 * 59 * <p>These are the current standard fields that can be used as extra data via 60 * {@link #subscribe(String, Bundle, SubscriptionCallback)}, {@link #unsubscribe(String, Bundle)}, 61 * and {@link SubscriptionCallback#onChildrenLoaded(String, List, Bundle)}. 62 * 63 * <ul> 64 * <li> {@link #EXTRA_PAGE} 65 * <li> {@link #EXTRA_PAGE_SIZE} 66 * </ul> 67 */ 68public final class MediaBrowser { 69 private static final String TAG = "MediaBrowser"; 70 private static final boolean DBG = false; 71 72 /** 73 * Used as an int extra field to denote the page number to subscribe. 74 * The value of {@code EXTRA_PAGE} should be greater than or equal to 1. 75 * 76 * @see #EXTRA_PAGE_SIZE 77 */ 78 public static final String EXTRA_PAGE = "android.media.browse.extra.PAGE"; 79 80 /** 81 * Used as an int extra field to denote the number of media items in a page. 82 * The value of {@code EXTRA_PAGE_SIZE} should be greater than or equal to 1. 83 * 84 * @see #EXTRA_PAGE 85 */ 86 public static final String EXTRA_PAGE_SIZE = "android.media.browse.extra.PAGE_SIZE"; 87 88 private static final int CONNECT_STATE_DISCONNECTED = 0; 89 private static final int CONNECT_STATE_CONNECTING = 1; 90 private static final int CONNECT_STATE_CONNECTED = 2; 91 private static final int CONNECT_STATE_SUSPENDED = 3; 92 93 private final Context mContext; 94 private final ComponentName mServiceComponent; 95 private final ConnectionCallback mCallback; 96 private final Bundle mRootHints; 97 private final Handler mHandler = new Handler(); 98 private final ArrayMap<String, Subscription> mSubscriptions = new ArrayMap<>(); 99 100 private volatile int mState = CONNECT_STATE_DISCONNECTED; 101 private volatile String mRootId; 102 private volatile MediaSession.Token mMediaSessionToken; 103 private volatile Bundle mExtras; 104 105 private MediaServiceConnection mServiceConnection; 106 private IMediaBrowserService mServiceBinder; 107 private IMediaBrowserServiceCallbacks mServiceCallbacks; 108 109 /** 110 * Creates a media browser for the specified media browse service. 111 * 112 * @param context The context. 113 * @param serviceComponent The component name of the media browse service. 114 * @param callback The connection callback. 115 * @param rootHints An optional bundle of service-specific arguments to send 116 * to the media browse service when connecting and retrieving the root id 117 * for browsing, or null if none. The contents of this bundle may affect 118 * the information returned when browsing. 119 * @see android.service.media.MediaBrowserService.BrowserRoot#EXTRA_RECENT 120 * @see android.service.media.MediaBrowserService.BrowserRoot#EXTRA_OFFLINE 121 * @see android.service.media.MediaBrowserService.BrowserRoot#EXTRA_SUGGESTED 122 */ 123 public MediaBrowser(Context context, ComponentName serviceComponent, 124 ConnectionCallback callback, Bundle rootHints) { 125 if (context == null) { 126 throw new IllegalArgumentException("context must not be null"); 127 } 128 if (serviceComponent == null) { 129 throw new IllegalArgumentException("service component must not be null"); 130 } 131 if (callback == null) { 132 throw new IllegalArgumentException("connection callback must not be null"); 133 } 134 mContext = context; 135 mServiceComponent = serviceComponent; 136 mCallback = callback; 137 mRootHints = rootHints; 138 } 139 140 /** 141 * Connects to the media browse service. 142 * <p> 143 * The connection callback specified in the constructor will be invoked 144 * when the connection completes or fails. 145 * </p> 146 */ 147 public void connect() { 148 if (mState != CONNECT_STATE_DISCONNECTED) { 149 throw new IllegalStateException("connect() called while not disconnected (state=" 150 + getStateLabel(mState) + ")"); 151 } 152 // TODO: remove this extra check. 153 if (DBG) { 154 if (mServiceConnection != null) { 155 throw new RuntimeException("mServiceConnection should be null. Instead it is " 156 + mServiceConnection); 157 } 158 } 159 if (mServiceBinder != null) { 160 throw new RuntimeException("mServiceBinder should be null. Instead it is " 161 + mServiceBinder); 162 } 163 if (mServiceCallbacks != null) { 164 throw new RuntimeException("mServiceCallbacks should be null. Instead it is " 165 + mServiceCallbacks); 166 } 167 168 mState = CONNECT_STATE_CONNECTING; 169 170 final Intent intent = new Intent(MediaBrowserService.SERVICE_INTERFACE); 171 intent.setComponent(mServiceComponent); 172 173 final ServiceConnection thisConnection = mServiceConnection = new MediaServiceConnection(); 174 175 boolean bound = false; 176 try { 177 bound = mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE); 178 } catch (Exception ex) { 179 Log.e(TAG, "Failed binding to service " + mServiceComponent); 180 } 181 182 if (!bound) { 183 // Tell them that it didn't work. We are already on the main thread, 184 // but we don't want to do callbacks inside of connect(). So post it, 185 // and then check that we are on the same ServiceConnection. We know 186 // we won't also get an onServiceConnected or onServiceDisconnected, 187 // so we won't be doing double callbacks. 188 mHandler.post(new Runnable() { 189 @Override 190 public void run() { 191 // Ensure that nobody else came in or tried to connect again. 192 if (thisConnection == mServiceConnection) { 193 forceCloseConnection(); 194 mCallback.onConnectionFailed(); 195 } 196 } 197 }); 198 } 199 200 if (DBG) { 201 Log.d(TAG, "connect..."); 202 dump(); 203 } 204 } 205 206 /** 207 * Disconnects from the media browse service. 208 * After this, no more callbacks will be received. 209 */ 210 public void disconnect() { 211 // It's ok to call this any state, because allowing this lets apps not have 212 // to check isConnected() unnecessarily. They won't appreciate the extra 213 // assertions for this. We do everything we can here to go back to a sane state. 214 mHandler.post(new Runnable() { 215 @Override 216 public void run() { 217 if (mServiceCallbacks != null) { 218 try { 219 mServiceBinder.disconnect(mServiceCallbacks); 220 } catch (RemoteException ex) { 221 // We are disconnecting anyway. Log, just for posterity but it's not 222 // a big problem. 223 Log.w(TAG, "RemoteException during connect for " + mServiceComponent); 224 } 225 } 226 forceCloseConnection(); 227 if (DBG) { 228 Log.d(TAG, "disconnect..."); 229 dump(); 230 } 231 } 232 }); 233 } 234 235 /** 236 * Null out the variables and unbind from the service. This doesn't include 237 * calling disconnect on the service, because we only try to do that in the 238 * clean shutdown cases. 239 * <p> 240 * Everywhere that calls this EXCEPT for disconnect() should follow it with 241 * a call to mCallback.onConnectionFailed(). Disconnect doesn't do that callback 242 * for a clean shutdown, but everywhere else is a dirty shutdown and should 243 * notify the app. 244 */ 245 private void forceCloseConnection() { 246 if (mServiceConnection != null) { 247 mContext.unbindService(mServiceConnection); 248 } 249 mState = CONNECT_STATE_DISCONNECTED; 250 mServiceConnection = null; 251 mServiceBinder = null; 252 mServiceCallbacks = null; 253 mRootId = null; 254 mMediaSessionToken = null; 255 } 256 257 /** 258 * Returns whether the browser is connected to the service. 259 */ 260 public boolean isConnected() { 261 return mState == CONNECT_STATE_CONNECTED; 262 } 263 264 /** 265 * Gets the service component that the media browser is connected to. 266 */ 267 public @NonNull ComponentName getServiceComponent() { 268 if (!isConnected()) { 269 throw new IllegalStateException("getServiceComponent() called while not connected" + 270 " (state=" + mState + ")"); 271 } 272 return mServiceComponent; 273 } 274 275 /** 276 * Gets the root id. 277 * <p> 278 * Note that the root id may become invalid or change when the 279 * browser is disconnected. 280 * </p> 281 * 282 * @throws IllegalStateException if not connected. 283 */ 284 public @NonNull String getRoot() { 285 if (!isConnected()) { 286 throw new IllegalStateException("getRoot() called while not connected (state=" 287 + getStateLabel(mState) + ")"); 288 } 289 return mRootId; 290 } 291 292 /** 293 * Gets any extras for the media service. 294 * 295 * @throws IllegalStateException if not connected. 296 */ 297 public @Nullable Bundle getExtras() { 298 if (!isConnected()) { 299 throw new IllegalStateException("getExtras() called while not connected (state=" 300 + getStateLabel(mState) + ")"); 301 } 302 return mExtras; 303 } 304 305 /** 306 * Gets the media session token associated with the media browser. 307 * <p> 308 * Note that the session token may become invalid or change when the 309 * browser is disconnected. 310 * </p> 311 * 312 * @return The session token for the browser, never null. 313 * 314 * @throws IllegalStateException if not connected. 315 */ 316 public @NonNull MediaSession.Token getSessionToken() { 317 if (!isConnected()) { 318 throw new IllegalStateException("getSessionToken() called while not connected (state=" 319 + mState + ")"); 320 } 321 return mMediaSessionToken; 322 } 323 324 /** 325 * Queries for information about the media items that are contained within 326 * the specified id and subscribes to receive updates when they change. 327 * <p> 328 * The list of subscriptions is maintained even when not connected and is 329 * restored after the reconnection. It is ok to subscribe while not connected 330 * but the results will not be returned until the connection completes. 331 * </p> 332 * <p> 333 * If the id is already subscribed with a different callback then the new 334 * callback will replace the previous one and the child data will be 335 * reloaded. 336 * </p> 337 * 338 * @param parentId The id of the parent media item whose list of children 339 * will be subscribed. 340 * @param callback The callback to receive the list of children. 341 */ 342 public void subscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) { 343 subscribeInternal(parentId, null, callback); 344 } 345 346 /** 347 * Queries with service-specific arguments for information about the media items 348 * that are contained within the specified id and subscribes to receive updates 349 * when they change. 350 * <p> 351 * The list of subscriptions is maintained even when not connected and is 352 * restored after the reconnection. It is ok to subscribe while not connected 353 * but the results will not be returned until the connection completes. 354 * </p> 355 * <p> 356 * If the id is already subscribed with a different callback then the new 357 * callback will replace the previous one and the child data will be 358 * reloaded. 359 * </p> 360 * 361 * @param parentId The id of the parent media item whose list of children 362 * will be subscribed. 363 * @param options A bundle of service-specific arguments to send to the media 364 * browse service. The contents of this bundle may affect the 365 * information returned when browsing. 366 * @param callback The callback to receive the list of children. 367 */ 368 public void subscribe(@NonNull String parentId, @NonNull Bundle options, 369 @NonNull SubscriptionCallback callback) { 370 if (options == null) { 371 throw new IllegalArgumentException("options are null"); 372 } 373 subscribeInternal(parentId, new Bundle(options), callback); 374 } 375 376 /** 377 * Unsubscribes for changes to the children of the specified media id. 378 * <p> 379 * The query callback will no longer be invoked for results associated with 380 * this id once this method returns. 381 * </p> 382 * 383 * @param parentId The id of the parent media item whose list of children 384 * will be unsubscribed. 385 */ 386 public void unsubscribe(@NonNull String parentId) { 387 unsubscribeInternal(parentId, null); 388 } 389 390 /** 391 * Unsubscribes for changes to the children of the specified media id. 392 * <p> 393 * The query callback will no longer be invoked for results associated with 394 * this id once this method returns. 395 * </p> 396 * 397 * @param parentId The id of the parent media item whose list of children 398 * will be unsubscribed. 399 * @param options A bundle sent to the media browse service to subscribe. 400 */ 401 public void unsubscribe(@NonNull String parentId, @NonNull Bundle options) { 402 if (options == null) { 403 throw new IllegalArgumentException("options are null"); 404 } 405 unsubscribeInternal(parentId, options); 406 } 407 408 /** 409 * Retrieves a specific {@link MediaItem} from the connected service. Not 410 * all services may support this, so falling back to subscribing to the 411 * parent's id should be used when unavailable. 412 * 413 * @param mediaId The id of the item to retrieve. 414 * @param cb The callback to receive the result on. 415 */ 416 public void getItem(final @NonNull String mediaId, @NonNull final ItemCallback cb) { 417 if (TextUtils.isEmpty(mediaId)) { 418 throw new IllegalArgumentException("mediaId is empty."); 419 } 420 if (cb == null) { 421 throw new IllegalArgumentException("cb is null."); 422 } 423 if (mState != CONNECT_STATE_CONNECTED) { 424 Log.i(TAG, "Not connected, unable to retrieve the MediaItem."); 425 mHandler.post(new Runnable() { 426 @Override 427 public void run() { 428 cb.onError(mediaId); 429 } 430 }); 431 return; 432 } 433 ResultReceiver receiver = new ResultReceiver(mHandler) { 434 @Override 435 protected void onReceiveResult(int resultCode, Bundle resultData) { 436 if (resultCode != 0 || resultData == null 437 || !resultData.containsKey(MediaBrowserService.KEY_MEDIA_ITEM)) { 438 cb.onError(mediaId); 439 return; 440 } 441 Parcelable item = resultData.getParcelable(MediaBrowserService.KEY_MEDIA_ITEM); 442 if (!(item instanceof MediaItem)) { 443 cb.onError(mediaId); 444 return; 445 } 446 cb.onItemLoaded((MediaItem)item); 447 } 448 }; 449 try { 450 mServiceBinder.getMediaItem(mediaId, receiver); 451 } catch (RemoteException e) { 452 Log.i(TAG, "Remote error getting media item."); 453 mHandler.post(new Runnable() { 454 @Override 455 public void run() { 456 cb.onError(mediaId); 457 } 458 }); 459 } 460 } 461 462 private void subscribeInternal(String parentId, Bundle options, SubscriptionCallback callback) { 463 // Check arguments. 464 if (TextUtils.isEmpty(parentId)) { 465 throw new IllegalArgumentException("parentId is empty."); 466 } 467 if (callback == null) { 468 throw new IllegalArgumentException("callback is null"); 469 } 470 // Update or create the subscription. 471 Subscription sub = mSubscriptions.get(parentId); 472 if (sub == null) { 473 sub = new Subscription(); 474 mSubscriptions.put(parentId, sub); 475 } 476 sub.putCallback(options, callback); 477 478 // If we are connected, tell the service that we are watching. If we aren't connected, 479 // the service will be told when we connect. 480 if (mState == CONNECT_STATE_CONNECTED) { 481 try { 482 // NOTE: Do not call addSubscriptionWithOptions when options are null. Otherwise, 483 // it will break the action of support library which expects addSubscription will 484 // be called when options are null. 485 if (options == null) { 486 mServiceBinder.addSubscription(parentId, mServiceCallbacks); 487 } else { 488 mServiceBinder.addSubscriptionWithOptions(parentId, options, mServiceCallbacks); 489 } 490 } catch (RemoteException ex) { 491 // Process is crashing. We will disconnect, and upon reconnect we will 492 // automatically reregister. So nothing to do here. 493 Log.d(TAG, "addSubscription failed with RemoteException parentId=" + parentId); 494 } 495 } 496 } 497 498 private void unsubscribeInternal(String parentId, Bundle options) { 499 // Check arguments. 500 if (TextUtils.isEmpty(parentId)) { 501 throw new IllegalArgumentException("parentId is empty."); 502 } 503 504 // Remove from our list. 505 Subscription sub = mSubscriptions.get(parentId); 506 507 // Tell the service if necessary. 508 if (sub != null && sub.removeCallback(options) && mState == CONNECT_STATE_CONNECTED) { 509 try { 510 // NOTE: Do not call removeSubscriptionWithOptions when options are null. Otherwise, 511 // it will break the action of support library which expects removeSubscription will 512 // be called when options are null. 513 if (options == null) { 514 mServiceBinder.removeSubscription(parentId, mServiceCallbacks); 515 } else { 516 mServiceBinder.removeSubscriptionWithOptions( 517 parentId, options, mServiceCallbacks); 518 } 519 } catch (RemoteException ex) { 520 // Process is crashing. We will disconnect, and upon reconnect we will 521 // automatically reregister. So nothing to do here. 522 Log.d(TAG, "removeSubscription failed with RemoteException parentId=" + parentId); 523 } 524 } 525 if (sub != null && sub.isEmpty()) { 526 mSubscriptions.remove(parentId); 527 } 528 } 529 530 /** 531 * For debugging. 532 */ 533 private static String getStateLabel(int state) { 534 switch (state) { 535 case CONNECT_STATE_DISCONNECTED: 536 return "CONNECT_STATE_DISCONNECTED"; 537 case CONNECT_STATE_CONNECTING: 538 return "CONNECT_STATE_CONNECTING"; 539 case CONNECT_STATE_CONNECTED: 540 return "CONNECT_STATE_CONNECTED"; 541 case CONNECT_STATE_SUSPENDED: 542 return "CONNECT_STATE_SUSPENDED"; 543 default: 544 return "UNKNOWN/" + state; 545 } 546 } 547 548 private final void onServiceConnected(final IMediaBrowserServiceCallbacks callback, 549 final String root, final MediaSession.Token session, final Bundle extra) { 550 mHandler.post(new Runnable() { 551 @Override 552 public void run() { 553 // Check to make sure there hasn't been a disconnect or a different 554 // ServiceConnection. 555 if (!isCurrent(callback, "onConnect")) { 556 return; 557 } 558 // Don't allow them to call us twice. 559 if (mState != CONNECT_STATE_CONNECTING) { 560 Log.w(TAG, "onConnect from service while mState=" 561 + getStateLabel(mState) + "... ignoring"); 562 return; 563 } 564 mRootId = root; 565 mMediaSessionToken = session; 566 mExtras = extra; 567 mState = CONNECT_STATE_CONNECTED; 568 569 if (DBG) { 570 Log.d(TAG, "ServiceCallbacks.onConnect..."); 571 dump(); 572 } 573 mCallback.onConnected(); 574 575 // we may receive some subscriptions before we are connected, so re-subscribe 576 // everything now 577 for (Entry<String, Subscription> subscriptionEntry : mSubscriptions.entrySet()) { 578 String id = subscriptionEntry.getKey(); 579 Subscription sub = subscriptionEntry.getValue(); 580 for (Bundle options : sub.getOptionsList()) { 581 try { 582 // NOTE: Do not call addSubscriptionWithOptions when options are null. 583 // Otherwise, it will break the action of support library which expects 584 // addSubscription will be called when options are null. 585 if (options == null) { 586 mServiceBinder.addSubscription(id, mServiceCallbacks); 587 } else { 588 mServiceBinder.addSubscriptionWithOptions( 589 id, options, mServiceCallbacks); 590 } 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 /** 861 * Called when the list of children is loaded or updated. 862 * 863 * @param parentId The media id of the parent media item. 864 * @param children The children which were loaded. 865 */ 866 public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children) { 867 } 868 869 /** 870 * Called when the list of children is loaded or updated. 871 * 872 * @param parentId The media id of the parent media item. 873 * @param children The children which were loaded. 874 * @param options A bundle of service-specific arguments sent to the media 875 * browse service. The contents of this bundle may affect the 876 * information returned when browsing. 877 */ 878 public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children, 879 @NonNull Bundle options) { 880 } 881 882 /** 883 * Called when the id doesn't exist or other errors in subscribing. 884 * <p> 885 * If this is called, the subscription remains until {@link MediaBrowser#unsubscribe} 886 * called, because some errors may heal themselves. 887 * </p> 888 * 889 * @param parentId The media id of the parent media item whose children could 890 * not be loaded. 891 */ 892 public void onError(@NonNull String parentId) { 893 } 894 895 /** 896 * Called when the id doesn't exist or other errors in subscribing. 897 * <p> 898 * If this is called, the subscription remains until {@link MediaBrowser#unsubscribe} 899 * called, because some errors may heal themselves. 900 * </p> 901 * 902 * @param parentId The media id of the parent media item whose children could 903 * not be loaded. 904 * @param options A bundle of service-specific arguments sent to the media 905 * browse service. 906 */ 907 public void onError(@NonNull String parentId, @NonNull Bundle options) { 908 } 909 } 910 911 /** 912 * Callback for receiving the result of {@link #getItem}. 913 */ 914 public static abstract class ItemCallback { 915 /** 916 * Called when the item has been returned by the browser service. 917 * 918 * @param item The item that was returned or null if it doesn't exist. 919 */ 920 public void onItemLoaded(MediaItem item) { 921 } 922 923 /** 924 * Called when the item doesn't exist or there was an error retrieving it. 925 * 926 * @param itemId The media id of the media item which could not be loaded. 927 */ 928 public void onError(@NonNull String itemId) { 929 } 930 } 931 932 /** 933 * ServiceConnection to the other app. 934 */ 935 private class MediaServiceConnection implements ServiceConnection { 936 @Override 937 public void onServiceConnected(final ComponentName name, final IBinder binder) { 938 postOrRun(new Runnable() { 939 @Override 940 public void run() { 941 if (DBG) { 942 Log.d(TAG, "MediaServiceConnection.onServiceConnected name=" + name 943 + " binder=" + binder); 944 dump(); 945 } 946 947 // Make sure we are still the current connection, and that they haven't called 948 // disconnect(). 949 if (!isCurrent("onServiceConnected")) { 950 return; 951 } 952 953 // Save their binder 954 mServiceBinder = IMediaBrowserService.Stub.asInterface(binder); 955 956 // We make a new mServiceCallbacks each time we connect so that we can drop 957 // responses from previous connections. 958 mServiceCallbacks = getNewServiceCallbacks(); 959 mState = CONNECT_STATE_CONNECTING; 960 961 // Call connect, which is async. When we get a response from that we will 962 // say that we're connected. 963 try { 964 if (DBG) { 965 Log.d(TAG, "ServiceCallbacks.onConnect..."); 966 dump(); 967 } 968 mServiceBinder.connect(mContext.getPackageName(), mRootHints, 969 mServiceCallbacks); 970 } catch (RemoteException ex) { 971 // Connect failed, which isn't good. But the auto-reconnect on the service 972 // will take over and we will come back. We will also get the 973 // onServiceDisconnected, which has all the cleanup code. So let that do 974 // it. 975 Log.w(TAG, "RemoteException during connect for " + mServiceComponent); 976 if (DBG) { 977 Log.d(TAG, "ServiceCallbacks.onConnect..."); 978 dump(); 979 } 980 } 981 } 982 }); 983 } 984 985 @Override 986 public void onServiceDisconnected(final ComponentName name) { 987 postOrRun(new Runnable() { 988 @Override 989 public void run() { 990 if (DBG) { 991 Log.d(TAG, "MediaServiceConnection.onServiceDisconnected name=" + name 992 + " this=" + this + " mServiceConnection=" + mServiceConnection); 993 dump(); 994 } 995 996 // Make sure we are still the current connection, and that they haven't called 997 // disconnect(). 998 if (!isCurrent("onServiceDisconnected")) { 999 return; 1000 } 1001 1002 // Clear out what we set in onServiceConnected 1003 mServiceBinder = null; 1004 mServiceCallbacks = null; 1005 1006 // And tell the app that it's suspended. 1007 mState = CONNECT_STATE_SUSPENDED; 1008 mCallback.onConnectionSuspended(); 1009 } 1010 }); 1011 } 1012 1013 private void postOrRun(Runnable r) { 1014 if (Thread.currentThread() == mHandler.getLooper().getThread()) { 1015 r.run(); 1016 } else { 1017 mHandler.post(r); 1018 } 1019 } 1020 1021 /** 1022 * Return true if this is the current ServiceConnection. Also logs if it's not. 1023 */ 1024 private boolean isCurrent(String funcName) { 1025 if (mServiceConnection != this) { 1026 if (mState != CONNECT_STATE_DISCONNECTED) { 1027 // Check mState, because otherwise this log is noisy. 1028 Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection=" 1029 + mServiceConnection + " this=" + this); 1030 } 1031 return false; 1032 } 1033 return true; 1034 } 1035 } 1036 1037 /** 1038 * Callbacks from the service. 1039 */ 1040 private static class ServiceCallbacks extends IMediaBrowserServiceCallbacks.Stub { 1041 private WeakReference<MediaBrowser> mMediaBrowser; 1042 1043 public ServiceCallbacks(MediaBrowser mediaBrowser) { 1044 mMediaBrowser = new WeakReference<MediaBrowser>(mediaBrowser); 1045 } 1046 1047 /** 1048 * The other side has acknowledged our connection. The parameters to this function 1049 * are the initial data as requested. 1050 */ 1051 @Override 1052 public void onConnect(String root, MediaSession.Token session, 1053 final Bundle extras) { 1054 MediaBrowser mediaBrowser = mMediaBrowser.get(); 1055 if (mediaBrowser != null) { 1056 mediaBrowser.onServiceConnected(this, root, session, extras); 1057 } 1058 } 1059 1060 /** 1061 * The other side does not like us. Tell the app via onConnectionFailed. 1062 */ 1063 @Override 1064 public void onConnectFailed() { 1065 MediaBrowser mediaBrowser = mMediaBrowser.get(); 1066 if (mediaBrowser != null) { 1067 mediaBrowser.onConnectionFailed(this); 1068 } 1069 } 1070 1071 @Override 1072 public void onLoadChildren(String parentId, ParceledListSlice list) { 1073 onLoadChildrenWithOptions(parentId, list, null); 1074 } 1075 1076 @Override 1077 public void onLoadChildrenWithOptions(String parentId, ParceledListSlice list, 1078 final Bundle options) { 1079 MediaBrowser mediaBrowser = mMediaBrowser.get(); 1080 if (mediaBrowser != null) { 1081 mediaBrowser.onLoadChildren(this, parentId, list, options); 1082 } 1083 } 1084 } 1085 1086 private static class Subscription { 1087 private final List<SubscriptionCallback> mCallbacks; 1088 private final List<Bundle> mOptionsList; 1089 1090 public Subscription() { 1091 mCallbacks = new ArrayList<>(); 1092 mOptionsList = new ArrayList<>(); 1093 } 1094 1095 public boolean isEmpty() { 1096 return mCallbacks.isEmpty(); 1097 } 1098 1099 public List<Bundle> getOptionsList() { 1100 return mOptionsList; 1101 } 1102 1103 public List<SubscriptionCallback> getCallbacks() { 1104 return mCallbacks; 1105 } 1106 1107 public SubscriptionCallback getCallback(Bundle options) { 1108 for (int i = 0; i < mOptionsList.size(); ++i) { 1109 if (MediaBrowserUtils.areSameOptions(mOptionsList.get(i), options)) { 1110 return mCallbacks.get(i); 1111 } 1112 } 1113 return null; 1114 } 1115 1116 public void putCallback(Bundle options, SubscriptionCallback callback) { 1117 for (int i = 0; i < mOptionsList.size(); ++i) { 1118 if (MediaBrowserUtils.areSameOptions(mOptionsList.get(i), options)) { 1119 mCallbacks.set(i, callback); 1120 return; 1121 } 1122 } 1123 mCallbacks.add(callback); 1124 mOptionsList.add(options); 1125 } 1126 1127 public boolean removeCallback(Bundle options) { 1128 for (int i = 0; i < mOptionsList.size(); ++i) { 1129 if (MediaBrowserUtils.areSameOptions(mOptionsList.get(i), options)) { 1130 mCallbacks.remove(i); 1131 mOptionsList.remove(i); 1132 return true; 1133 } 1134 } 1135 return false; 1136 } 1137 } 1138} 1139