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.service.media.MediaBrowserService; 37import android.service.media.IMediaBrowserService; 38import android.service.media.IMediaBrowserServiceCallbacks; 39import android.text.TextUtils; 40import android.util.ArrayMap; 41import android.util.Log; 42 43import java.lang.annotation.Retention; 44import java.lang.annotation.RetentionPolicy; 45import java.lang.ref.WeakReference; 46import java.util.Collections; 47import java.util.List; 48 49/** 50 * Browses media content offered by a link MediaBrowserService. 51 * <p> 52 * This object is not thread-safe. All calls should happen on the thread on which the browser 53 * was constructed. 54 * </p> 55 */ 56public final class MediaBrowser { 57 private static final String TAG = "MediaBrowser"; 58 private static final boolean DBG = false; 59 60 private static final int CONNECT_STATE_DISCONNECTED = 0; 61 private static final int CONNECT_STATE_CONNECTING = 1; 62 private static final int CONNECT_STATE_CONNECTED = 2; 63 private static final int CONNECT_STATE_SUSPENDED = 3; 64 65 private final Context mContext; 66 private final ComponentName mServiceComponent; 67 private final ConnectionCallback mCallback; 68 private final Bundle mRootHints; 69 private final Handler mHandler = new Handler(); 70 private final ArrayMap<String,Subscription> mSubscriptions = 71 new ArrayMap<String, MediaBrowser.Subscription>(); 72 73 private int mState = CONNECT_STATE_DISCONNECTED; 74 private MediaServiceConnection mServiceConnection; 75 private IMediaBrowserService mServiceBinder; 76 private IMediaBrowserServiceCallbacks mServiceCallbacks; 77 private String mRootId; 78 private MediaSession.Token mMediaSessionToken; 79 private Bundle mExtras; 80 81 /** 82 * Creates a media browser for the specified media browse service. 83 * 84 * @param context The context. 85 * @param serviceComponent The component name of the media browse service. 86 * @param callback The connection callback. 87 * @param rootHints An optional bundle of service-specific arguments to send 88 * to the media browse service when connecting and retrieving the root id 89 * for browsing, or null if none. The contents of this bundle may affect 90 * the information returned when browsing. 91 */ 92 public MediaBrowser(Context context, ComponentName serviceComponent, 93 ConnectionCallback callback, Bundle rootHints) { 94 if (context == null) { 95 throw new IllegalArgumentException("context must not be null"); 96 } 97 if (serviceComponent == null) { 98 throw new IllegalArgumentException("service component must not be null"); 99 } 100 if (callback == null) { 101 throw new IllegalArgumentException("connection callback must not be null"); 102 } 103 mContext = context; 104 mServiceComponent = serviceComponent; 105 mCallback = callback; 106 mRootHints = rootHints; 107 } 108 109 /** 110 * Connects to the media browse service. 111 * <p> 112 * The connection callback specified in the constructor will be invoked 113 * when the connection completes or fails. 114 * </p> 115 */ 116 public void connect() { 117 if (mState != CONNECT_STATE_DISCONNECTED) { 118 throw new IllegalStateException("connect() called while not disconnected (state=" 119 + getStateLabel(mState) + ")"); 120 } 121 // TODO: remove this extra check. 122 if (DBG) { 123 if (mServiceConnection != null) { 124 throw new RuntimeException("mServiceConnection should be null. Instead it is " 125 + mServiceConnection); 126 } 127 } 128 if (mServiceBinder != null) { 129 throw new RuntimeException("mServiceBinder should be null. Instead it is " 130 + mServiceBinder); 131 } 132 if (mServiceCallbacks != null) { 133 throw new RuntimeException("mServiceCallbacks should be null. Instead it is " 134 + mServiceCallbacks); 135 } 136 137 mState = CONNECT_STATE_CONNECTING; 138 139 final Intent intent = new Intent(MediaBrowserService.SERVICE_INTERFACE); 140 intent.setComponent(mServiceComponent); 141 142 final ServiceConnection thisConnection = mServiceConnection = new MediaServiceConnection(); 143 144 boolean bound = false; 145 try { 146 bound = mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE); 147 } catch (Exception ex) { 148 Log.e(TAG, "Failed binding to service " + mServiceComponent); 149 } 150 151 if (!bound) { 152 // Tell them that it didn't work. We are already on the main thread, 153 // but we don't want to do callbacks inside of connect(). So post it, 154 // and then check that we are on the same ServiceConnection. We know 155 // we won't also get an onServiceConnected or onServiceDisconnected, 156 // so we won't be doing double callbacks. 157 mHandler.post(new Runnable() { 158 @Override 159 public void run() { 160 // Ensure that nobody else came in or tried to connect again. 161 if (thisConnection == mServiceConnection) { 162 forceCloseConnection(); 163 mCallback.onConnectionFailed(); 164 } 165 } 166 }); 167 } 168 169 if (DBG) { 170 Log.d(TAG, "connect..."); 171 dump(); 172 } 173 } 174 175 /** 176 * Disconnects from the media browse service. 177 * After this, no more callbacks will be received. 178 */ 179 public void disconnect() { 180 // It's ok to call this any state, because allowing this lets apps not have 181 // to check isConnected() unnecessarily. They won't appreciate the extra 182 // assertions for this. We do everything we can here to go back to a sane state. 183 if (mServiceCallbacks != null) { 184 try { 185 mServiceBinder.disconnect(mServiceCallbacks); 186 } catch (RemoteException ex) { 187 // We are disconnecting anyway. Log, just for posterity but it's not 188 // a big problem. 189 Log.w(TAG, "RemoteException during connect for " + mServiceComponent); 190 } 191 } 192 forceCloseConnection(); 193 194 if (DBG) { 195 Log.d(TAG, "disconnect..."); 196 dump(); 197 } 198 } 199 200 /** 201 * Null out the variables and unbind from the service. This doesn't include 202 * calling disconnect on the service, because we only try to do that in the 203 * clean shutdown cases. 204 * <p> 205 * Everywhere that calls this EXCEPT for disconnect() should follow it with 206 * a call to mCallback.onConnectionFailed(). Disconnect doesn't do that callback 207 * for a clean shutdown, but everywhere else is a dirty shutdown and should 208 * notify the app. 209 */ 210 private void forceCloseConnection() { 211 if (mServiceConnection != null) { 212 mContext.unbindService(mServiceConnection); 213 } 214 mState = CONNECT_STATE_DISCONNECTED; 215 mServiceConnection = null; 216 mServiceBinder = null; 217 mServiceCallbacks = null; 218 mRootId = null; 219 mMediaSessionToken = null; 220 } 221 222 /** 223 * Returns whether the browser is connected to the service. 224 */ 225 public boolean isConnected() { 226 return mState == CONNECT_STATE_CONNECTED; 227 } 228 229 /** 230 * Gets the service component that the media browser is connected to. 231 */ 232 public @NonNull ComponentName getServiceComponent() { 233 if (!isConnected()) { 234 throw new IllegalStateException("getServiceComponent() called while not connected" + 235 " (state=" + mState + ")"); 236 } 237 return mServiceComponent; 238 } 239 240 /** 241 * Gets the root id. 242 * <p> 243 * Note that the root id may become invalid or change when when the 244 * browser is disconnected. 245 * </p> 246 * 247 * @throws IllegalStateException if not connected. 248 */ 249 public @NonNull String getRoot() { 250 if (!isConnected()) { 251 throw new IllegalStateException("getSessionToken() called while not connected (state=" 252 + getStateLabel(mState) + ")"); 253 } 254 return mRootId; 255 } 256 257 /** 258 * Gets any extras for the media service. 259 * 260 * @throws IllegalStateException if not connected. 261 */ 262 public @Nullable Bundle getExtras() { 263 if (!isConnected()) { 264 throw new IllegalStateException("getExtras() called while not connected (state=" 265 + getStateLabel(mState) + ")"); 266 } 267 return mExtras; 268 } 269 270 /** 271 * Gets the media session token associated with the media browser. 272 * <p> 273 * Note that the session token may become invalid or change when when the 274 * browser is disconnected. 275 * </p> 276 * 277 * @return The session token for the browser, never null. 278 * 279 * @throws IllegalStateException if not connected. 280 */ 281 public @NonNull MediaSession.Token getSessionToken() { 282 if (!isConnected()) { 283 throw new IllegalStateException("getSessionToken() called while not connected (state=" 284 + mState + ")"); 285 } 286 return mMediaSessionToken; 287 } 288 289 /** 290 * Queries for information about the media items that are contained within 291 * the specified id and subscribes to receive updates when they change. 292 * <p> 293 * The list of subscriptions is maintained even when not connected and is 294 * restored after reconnection. It is ok to subscribe while not connected 295 * but the results will not be returned until the connection completes. 296 * </p><p> 297 * If the id is already subscribed with a different callback then the new 298 * callback will replace the previous one. 299 * </p> 300 * 301 * @param parentId The id of the parent media item whose list of children 302 * will be subscribed. 303 * @param callback The callback to receive the list of children. 304 */ 305 public void subscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) { 306 // Check arguments. 307 if (parentId == null) { 308 throw new IllegalArgumentException("parentId is null"); 309 } 310 if (callback == null) { 311 throw new IllegalArgumentException("callback is null"); 312 } 313 314 // Update or create the subscription. 315 Subscription sub = mSubscriptions.get(parentId); 316 boolean newSubscription = sub == null; 317 if (newSubscription) { 318 sub = new Subscription(parentId); 319 mSubscriptions.put(parentId, sub); 320 } 321 sub.callback = callback; 322 323 // If we are connected, tell the service that we are watching. If we aren't 324 // connected, the service will be told when we connect. 325 if (mState == CONNECT_STATE_CONNECTED && newSubscription) { 326 try { 327 mServiceBinder.addSubscription(parentId, mServiceCallbacks); 328 } catch (RemoteException ex) { 329 // Process is crashing. We will disconnect, and upon reconnect we will 330 // automatically reregister. So nothing to do here. 331 Log.d(TAG, "addSubscription failed with RemoteException parentId=" + parentId); 332 } 333 } 334 } 335 336 /** 337 * Unsubscribes for changes to the children of the specified media id. 338 * <p> 339 * The query callback will no longer be invoked for results associated with 340 * this id once this method returns. 341 * </p> 342 * 343 * @param parentId The id of the parent media item whose list of children 344 * will be unsubscribed. 345 */ 346 public void unsubscribe(@NonNull String parentId) { 347 // Check arguments. 348 if (parentId == null) { 349 throw new IllegalArgumentException("parentId is null"); 350 } 351 352 // Remove from our list. 353 final Subscription sub = mSubscriptions.remove(parentId); 354 355 // Tell the service if necessary. 356 if (mState == CONNECT_STATE_CONNECTED && sub != null) { 357 try { 358 mServiceBinder.removeSubscription(parentId, mServiceCallbacks); 359 } catch (RemoteException ex) { 360 // Process is crashing. We will disconnect, and upon reconnect we will 361 // automatically reregister. So nothing to do here. 362 Log.d(TAG, "removeSubscription failed with RemoteException parentId=" + parentId); 363 } 364 } 365 } 366 367 /** 368 * For debugging. 369 */ 370 private static String getStateLabel(int state) { 371 switch (state) { 372 case CONNECT_STATE_DISCONNECTED: 373 return "CONNECT_STATE_DISCONNECTED"; 374 case CONNECT_STATE_CONNECTING: 375 return "CONNECT_STATE_CONNECTING"; 376 case CONNECT_STATE_CONNECTED: 377 return "CONNECT_STATE_CONNECTED"; 378 case CONNECT_STATE_SUSPENDED: 379 return "CONNECT_STATE_SUSPENDED"; 380 default: 381 return "UNKNOWN/" + state; 382 } 383 } 384 385 private final void onServiceConnected(final IMediaBrowserServiceCallbacks callback, 386 final String root, final MediaSession.Token session, final Bundle extra) { 387 mHandler.post(new Runnable() { 388 @Override 389 public void run() { 390 // Check to make sure there hasn't been a disconnect or a different 391 // ServiceConnection. 392 if (!isCurrent(callback, "onConnect")) { 393 return; 394 } 395 // Don't allow them to call us twice. 396 if (mState != CONNECT_STATE_CONNECTING) { 397 Log.w(TAG, "onConnect from service while mState=" 398 + getStateLabel(mState) + "... ignoring"); 399 return; 400 } 401 mRootId = root; 402 mMediaSessionToken = session; 403 mExtras = extra; 404 mState = CONNECT_STATE_CONNECTED; 405 406 if (DBG) { 407 Log.d(TAG, "ServiceCallbacks.onConnect..."); 408 dump(); 409 } 410 mCallback.onConnected(); 411 412 // we may receive some subscriptions before we are connected, so re-subscribe 413 // everything now 414 for (String id : mSubscriptions.keySet()) { 415 try { 416 mServiceBinder.addSubscription(id, mServiceCallbacks); 417 } catch (RemoteException ex) { 418 // Process is crashing. We will disconnect, and upon reconnect we will 419 // automatically reregister. So nothing to do here. 420 Log.d(TAG, "addSubscription failed with RemoteException parentId=" + id); 421 } 422 } 423 } 424 }); 425 } 426 427 private final void onConnectionFailed(final IMediaBrowserServiceCallbacks callback) { 428 mHandler.post(new Runnable() { 429 @Override 430 public void run() { 431 Log.e(TAG, "onConnectFailed for " + mServiceComponent); 432 433 // Check to make sure there hasn't been a disconnect or a different 434 // ServiceConnection. 435 if (!isCurrent(callback, "onConnectFailed")) { 436 return; 437 } 438 // Don't allow them to call us twice. 439 if (mState != CONNECT_STATE_CONNECTING) { 440 Log.w(TAG, "onConnect from service while mState=" 441 + getStateLabel(mState) + "... ignoring"); 442 return; 443 } 444 445 // Clean up 446 forceCloseConnection(); 447 448 // Tell the app. 449 mCallback.onConnectionFailed(); 450 } 451 }); 452 } 453 454 private final void onLoadChildren(final IMediaBrowserServiceCallbacks callback, 455 final String parentId, final ParceledListSlice list) { 456 mHandler.post(new Runnable() { 457 @Override 458 public void run() { 459 // Check that there hasn't been a disconnect or a different 460 // ServiceConnection. 461 if (!isCurrent(callback, "onLoadChildren")) { 462 return; 463 } 464 465 List<MediaItem> data = list.getList(); 466 if (DBG) { 467 Log.d(TAG, "onLoadChildren for " + mServiceComponent + " id=" + parentId); 468 } 469 if (data == null) { 470 data = Collections.emptyList(); 471 } 472 473 // Check that the subscription is still subscribed. 474 final Subscription subscription = mSubscriptions.get(parentId); 475 if (subscription == null) { 476 if (DBG) { 477 Log.d(TAG, "onLoadChildren for id that isn't subscribed id=" 478 + parentId); 479 } 480 return; 481 } 482 483 // Tell the app. 484 subscription.callback.onChildrenLoaded(parentId, data); 485 } 486 }); 487 } 488 489 /** 490 * Return true if {@code callback} is the current ServiceCallbacks. Also logs if it's not. 491 */ 492 private boolean isCurrent(IMediaBrowserServiceCallbacks callback, String funcName) { 493 if (mServiceCallbacks != callback) { 494 if (mState != CONNECT_STATE_DISCONNECTED) { 495 Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection=" 496 + mServiceCallbacks + " this=" + this); 497 } 498 return false; 499 } 500 return true; 501 } 502 503 private ServiceCallbacks getNewServiceCallbacks() { 504 return new ServiceCallbacks(this); 505 } 506 507 /** 508 * Log internal state. 509 * @hide 510 */ 511 void dump() { 512 Log.d(TAG, "MediaBrowser..."); 513 Log.d(TAG, " mServiceComponent=" + mServiceComponent); 514 Log.d(TAG, " mCallback=" + mCallback); 515 Log.d(TAG, " mRootHints=" + mRootHints); 516 Log.d(TAG, " mState=" + getStateLabel(mState)); 517 Log.d(TAG, " mServiceConnection=" + mServiceConnection); 518 Log.d(TAG, " mServiceBinder=" + mServiceBinder); 519 Log.d(TAG, " mServiceCallbacks=" + mServiceCallbacks); 520 Log.d(TAG, " mRootId=" + mRootId); 521 Log.d(TAG, " mMediaSessionToken=" + mMediaSessionToken); 522 } 523 524 public static class MediaItem implements Parcelable { 525 private final int mFlags; 526 private final MediaDescription mDescription; 527 528 /** @hide */ 529 @Retention(RetentionPolicy.SOURCE) 530 @IntDef(flag=true, value = { FLAG_BROWSABLE, FLAG_PLAYABLE }) 531 public @interface Flags { } 532 533 /** 534 * Flag: Indicates that the item has children of its own. 535 */ 536 public static final int FLAG_BROWSABLE = 1 << 0; 537 538 /** 539 * Flag: Indicates that the item is playable. 540 * <p> 541 * The id of this item may be passed to 542 * {@link MediaController.TransportControls#playFromMediaId(String, Bundle)} 543 * to start playing it. 544 * </p> 545 */ 546 public static final int FLAG_PLAYABLE = 1 << 1; 547 548 /** 549 * Create a new MediaItem for use in browsing media. 550 * @param description The description of the media, which must include a 551 * media id. 552 * @param flags The flags for this item. 553 */ 554 public MediaItem(@NonNull MediaDescription description, @Flags int flags) { 555 if (description == null) { 556 throw new IllegalArgumentException("description cannot be null"); 557 } 558 if (TextUtils.isEmpty(description.getMediaId())) { 559 throw new IllegalArgumentException("description must have a non-empty media id"); 560 } 561 mFlags = flags; 562 mDescription = description; 563 } 564 565 /** 566 * Private constructor. 567 */ 568 private MediaItem(Parcel in) { 569 mFlags = in.readInt(); 570 mDescription = MediaDescription.CREATOR.createFromParcel(in); 571 } 572 573 @Override 574 public int describeContents() { 575 return 0; 576 } 577 578 @Override 579 public void writeToParcel(Parcel out, int flags) { 580 out.writeInt(mFlags); 581 mDescription.writeToParcel(out, flags); 582 } 583 584 @Override 585 public String toString() { 586 final StringBuilder sb = new StringBuilder("MediaItem{"); 587 sb.append("mFlags=").append(mFlags); 588 sb.append(", mDescription=").append(mDescription); 589 sb.append('}'); 590 return sb.toString(); 591 } 592 593 public static final Parcelable.Creator<MediaItem> CREATOR = 594 new Parcelable.Creator<MediaItem>() { 595 @Override 596 public MediaItem createFromParcel(Parcel in) { 597 return new MediaItem(in); 598 } 599 600 @Override 601 public MediaItem[] newArray(int size) { 602 return new MediaItem[size]; 603 } 604 }; 605 606 /** 607 * Gets the flags of the item. 608 */ 609 public @Flags int getFlags() { 610 return mFlags; 611 } 612 613 /** 614 * Returns whether this item is browsable. 615 * @see #FLAG_BROWSABLE 616 */ 617 public boolean isBrowsable() { 618 return (mFlags & FLAG_BROWSABLE) != 0; 619 } 620 621 /** 622 * Returns whether this item is playable. 623 * @see #FLAG_PLAYABLE 624 */ 625 public boolean isPlayable() { 626 return (mFlags & FLAG_PLAYABLE) != 0; 627 } 628 629 /** 630 * Returns the description of the media. 631 */ 632 public @NonNull MediaDescription getDescription() { 633 return mDescription; 634 } 635 636 /** 637 * Returns the media id for this item. 638 */ 639 public @NonNull String getMediaId() { 640 return mDescription.getMediaId(); 641 } 642 } 643 644 645 /** 646 * Callbacks for connection related events. 647 */ 648 public static class ConnectionCallback { 649 /** 650 * Invoked after {@link MediaBrowser#connect()} when the request has successfully completed. 651 */ 652 public void onConnected() { 653 } 654 655 /** 656 * Invoked when the client is disconnected from the media browser. 657 */ 658 public void onConnectionSuspended() { 659 } 660 661 /** 662 * Invoked when the connection to the media browser failed. 663 */ 664 public void onConnectionFailed() { 665 } 666 } 667 668 /** 669 * Callbacks for subscription related events. 670 */ 671 public static abstract class SubscriptionCallback { 672 /** 673 * Called when the list of children is loaded or updated. 674 */ 675 public void onChildrenLoaded(@NonNull String parentId, 676 @NonNull List<MediaItem> children) { 677 } 678 679 /** 680 * Called when the id doesn't exist or other errors in subscribing. 681 * <p> 682 * If this is called, the subscription remains until {@link MediaBrowser#unsubscribe} 683 * called, because some errors may heal themselves. 684 * </p> 685 */ 686 public void onError(@NonNull String id) { 687 } 688 } 689 690 /** 691 * ServiceConnection to the other app. 692 */ 693 private class MediaServiceConnection implements ServiceConnection { 694 @Override 695 public void onServiceConnected(ComponentName name, IBinder binder) { 696 if (DBG) { 697 Log.d(TAG, "MediaServiceConnection.onServiceConnected name=" + name 698 + " binder=" + binder); 699 dump(); 700 } 701 702 // Make sure we are still the current connection, and that they haven't called 703 // disconnect(). 704 if (!isCurrent("onServiceConnected")) { 705 return; 706 } 707 708 // Save their binder 709 mServiceBinder = IMediaBrowserService.Stub.asInterface(binder); 710 711 // We make a new mServiceCallbacks each time we connect so that we can drop 712 // responses from previous connections. 713 mServiceCallbacks = getNewServiceCallbacks(); 714 mState = CONNECT_STATE_CONNECTING; 715 716 // Call connect, which is async. When we get a response from that we will 717 // say that we're connected. 718 try { 719 if (DBG) { 720 Log.d(TAG, "ServiceCallbacks.onConnect..."); 721 dump(); 722 } 723 mServiceBinder.connect(mContext.getPackageName(), mRootHints, mServiceCallbacks); 724 } catch (RemoteException ex) { 725 // Connect failed, which isn't good. But the auto-reconnect on the service 726 // will take over and we will come back. We will also get the 727 // onServiceDisconnected, which has all the cleanup code. So let that do it. 728 Log.w(TAG, "RemoteException during connect for " + mServiceComponent); 729 if (DBG) { 730 Log.d(TAG, "ServiceCallbacks.onConnect..."); 731 dump(); 732 } 733 } 734 } 735 736 @Override 737 public void onServiceDisconnected(ComponentName name) { 738 if (DBG) { 739 Log.d(TAG, "MediaServiceConnection.onServiceDisconnected name=" + name 740 + " this=" + this + " mServiceConnection=" + mServiceConnection); 741 dump(); 742 } 743 744 // Make sure we are still the current connection, and that they haven't called 745 // disconnect(). 746 if (!isCurrent("onServiceDisconnected")) { 747 return; 748 } 749 750 // Clear out what we set in onServiceConnected 751 mServiceBinder = null; 752 mServiceCallbacks = null; 753 754 // And tell the app that it's suspended. 755 mState = CONNECT_STATE_SUSPENDED; 756 mCallback.onConnectionSuspended(); 757 } 758 759 /** 760 * Return true if this is the current ServiceConnection. Also logs if it's not. 761 */ 762 private boolean isCurrent(String funcName) { 763 if (mServiceConnection != this) { 764 if (mState != CONNECT_STATE_DISCONNECTED) { 765 // Check mState, because otherwise this log is noisy. 766 Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection=" 767 + mServiceConnection + " this=" + this); 768 } 769 return false; 770 } 771 return true; 772 } 773 } 774 775 /** 776 * Callbacks from the service. 777 */ 778 private static class ServiceCallbacks extends IMediaBrowserServiceCallbacks.Stub { 779 private WeakReference<MediaBrowser> mMediaBrowser; 780 781 public ServiceCallbacks(MediaBrowser mediaBrowser) { 782 mMediaBrowser = new WeakReference<MediaBrowser>(mediaBrowser); 783 } 784 785 /** 786 * The other side has acknowledged our connection. The parameters to this function 787 * are the initial data as requested. 788 */ 789 @Override 790 public void onConnect(final String root, final MediaSession.Token session, 791 final Bundle extras) { 792 MediaBrowser mediaBrowser = mMediaBrowser.get(); 793 if (mediaBrowser != null) { 794 mediaBrowser.onServiceConnected(this, root, session, extras); 795 } 796 } 797 798 /** 799 * The other side does not like us. Tell the app via onConnectionFailed. 800 */ 801 @Override 802 public void onConnectFailed() { 803 MediaBrowser mediaBrowser = mMediaBrowser.get(); 804 if (mediaBrowser != null) { 805 mediaBrowser.onConnectionFailed(this); 806 } 807 } 808 809 @Override 810 public void onLoadChildren(final String parentId, final ParceledListSlice list) { 811 MediaBrowser mediaBrowser = mMediaBrowser.get(); 812 if (mediaBrowser != null) { 813 mediaBrowser.onLoadChildren(this, parentId, list); 814 } 815 } 816 } 817 818 private static class Subscription { 819 final String id; 820 SubscriptionCallback callback; 821 822 Subscription(String id) { 823 this.id = id; 824 } 825 } 826} 827