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