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