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