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