MediaBrowserServiceCompat.java revision 82387d9f1b1ed30028e4c9090ec5fadce7d146f1
1/* 2 * Copyright (C) 2015 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.support.v4.media; 18 19import android.app.Service; 20import android.content.Intent; 21import android.content.pm.PackageManager; 22import android.os.Binder; 23import android.os.Build; 24import android.os.Bundle; 25import android.os.Handler; 26import android.os.IBinder; 27import android.os.Message; 28import android.os.Messenger; 29import android.os.Parcel; 30import android.os.RemoteException; 31import android.support.annotation.IntDef; 32import android.support.annotation.NonNull; 33import android.support.annotation.Nullable; 34import android.support.v4.app.BundleCompat; 35import android.support.v4.media.session.MediaSessionCompat; 36import android.support.v4.os.ResultReceiver; 37import android.support.v4.util.ArrayMap; 38import android.text.TextUtils; 39import android.util.Log; 40 41import java.io.FileDescriptor; 42import java.io.PrintWriter; 43import java.lang.annotation.Retention; 44import java.lang.annotation.RetentionPolicy; 45import java.util.ArrayList; 46import java.util.Collections; 47import java.util.HashMap; 48import java.util.List; 49 50import static android.support.v4.media.MediaBrowserProtocol.*; 51 52/** 53 * Base class for media browse services. 54 * <p> 55 * Media browse services enable applications to browse media content provided by an application 56 * and ask the application to start playing it. They may also be used to control content that 57 * is already playing by way of a {@link MediaSessionCompat}. 58 * </p> 59 * 60 * To extend this class, you must declare the service in your manifest file with 61 * an intent filter with the {@link #SERVICE_INTERFACE} action. 62 * 63 * For example: 64 * </p><pre> 65 * <service android:name=".MyMediaBrowserServiceCompat" 66 * android:label="@string/service_name" > 67 * <intent-filter> 68 * <action android:name="android.media.browse.MediaBrowserService" /> 69 * </intent-filter> 70 * </service> 71 * </pre> 72 */ 73public abstract class MediaBrowserServiceCompat extends Service { 74 private static final String TAG = "MediaBrowserServiceCompat"; 75 private static final boolean DBG = false; 76 77 private MediaBrowserServiceImpl mImpl; 78 79 /** 80 * The {@link Intent} that must be declared as handled by the service. 81 */ 82 public static final String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService"; 83 84 /** 85 * A key for passing the MediaItem to the ResultReceiver in getItem. 86 * 87 * @hide 88 */ 89 public static final String KEY_MEDIA_ITEM = "media_item"; 90 91 private static final int RESULT_FLAG_OPTION_NOT_HANDLED = 0x00000001; 92 93 /** @hide */ 94 @Retention(RetentionPolicy.SOURCE) 95 @IntDef(flag=true, value = { RESULT_FLAG_OPTION_NOT_HANDLED }) 96 private @interface ResultFlags { } 97 98 private final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap<>(); 99 private final ServiceHandler mHandler = new ServiceHandler(); 100 MediaSessionCompat.Token mSession; 101 102 interface MediaBrowserServiceImpl { 103 void onCreate(); 104 IBinder onBind(Intent intent); 105 } 106 107 class MediaBrowserServiceImplBase implements MediaBrowserServiceImpl { 108 private Messenger mMessenger; 109 110 @Override 111 public void onCreate() { 112 mMessenger = new Messenger(mHandler); 113 } 114 115 @Override 116 public IBinder onBind(Intent intent) { 117 if (SERVICE_INTERFACE.equals(intent.getAction())) { 118 return mMessenger.getBinder(); 119 } 120 return null; 121 } 122 } 123 124 class MediaBrowserServiceImplApi21 implements MediaBrowserServiceImpl { 125 private Object mServiceObj; 126 127 @Override 128 public void onCreate() { 129 mServiceObj = MediaBrowserServiceCompatApi21.createService(); 130 MediaBrowserServiceCompatApi21.onCreate(mServiceObj, new ServiceImplApi21()); 131 } 132 133 @Override 134 public IBinder onBind(Intent intent) { 135 return MediaBrowserServiceCompatApi21.onBind(mServiceObj, intent); 136 } 137 } 138 139 class MediaBrowserServiceImplApi23 implements MediaBrowserServiceImpl { 140 private Object mServiceObj; 141 142 @Override 143 public void onCreate() { 144 mServiceObj = MediaBrowserServiceCompatApi23.createService(); 145 MediaBrowserServiceCompatApi23.onCreate(mServiceObj, new ServiceImplApi23()); 146 } 147 148 @Override 149 public IBinder onBind(Intent intent) { 150 return MediaBrowserServiceCompatApi23.onBind(mServiceObj, intent); 151 } 152 } 153 154 private final class ServiceHandler extends Handler { 155 private final ServiceImpl mServiceImpl = new ServiceImpl(); 156 157 @Override 158 public void handleMessage(Message msg) { 159 Bundle data = msg.getData(); 160 switch (msg.what) { 161 case CLIENT_MSG_CONNECT: 162 mServiceImpl.connect(data.getString(DATA_PACKAGE_NAME), 163 data.getInt(DATA_CALLING_UID), data.getBundle(DATA_ROOT_HINTS), 164 new ServiceCallbacksCompat(msg.replyTo)); 165 break; 166 case CLIENT_MSG_DISCONNECT: 167 mServiceImpl.disconnect(new ServiceCallbacksCompat(msg.replyTo)); 168 break; 169 case CLIENT_MSG_ADD_SUBSCRIPTION: 170 mServiceImpl.addSubscription(data.getString(DATA_MEDIA_ITEM_ID), 171 data.getBundle(DATA_OPTIONS), new ServiceCallbacksCompat(msg.replyTo)); 172 break; 173 case CLIENT_MSG_REMOVE_SUBSCRIPTION: 174 mServiceImpl.removeSubscription(data.getString(DATA_MEDIA_ITEM_ID), 175 data.getBundle(DATA_OPTIONS), new ServiceCallbacksCompat(msg.replyTo)); 176 break; 177 case CLIENT_MSG_GET_MEDIA_ITEM: 178 mServiceImpl.getMediaItem(data.getString(DATA_MEDIA_ITEM_ID), 179 (ResultReceiver) data.getParcelable(DATA_RESULT_RECEIVER)); 180 break; 181 default: 182 Log.w(TAG, "Unhandled message: " + msg 183 + "\n Service version: " + SERVICE_VERSION_CURRENT 184 + "\n Client version: " + msg.arg1); 185 } 186 } 187 188 @Override 189 public boolean sendMessageAtTime(Message msg, long uptimeMillis) { 190 // Binder.getCallingUid() in handleMessage will return the uid of this process. 191 // In order to get the right calling uid, Binder.getCallingUid() should be called here. 192 Bundle data = msg.getData(); 193 data.setClassLoader(MediaBrowserCompat.class.getClassLoader()); 194 data.putInt(DATA_CALLING_UID, Binder.getCallingUid()); 195 return super.sendMessageAtTime(msg, uptimeMillis); 196 } 197 198 public void postOrRun(Runnable r) { 199 if (Thread.currentThread() == getLooper().getThread()) { 200 r.run(); 201 } else { 202 post(r); 203 } 204 } 205 206 public ServiceImpl getServiceImpl() { 207 return mServiceImpl; 208 } 209 } 210 211 /** 212 * All the info about a connection. 213 */ 214 private class ConnectionRecord { 215 String pkg; 216 Bundle rootHints; 217 ServiceCallbacks callbacks; 218 BrowserRoot root; 219 HashMap<String, List<Bundle>> subscriptions = new HashMap(); 220 } 221 222 /** 223 * Completion handler for asynchronous callback methods in {@link MediaBrowserServiceCompat}. 224 * <p> 225 * Each of the methods that takes one of these to send the result must call 226 * {@link #sendResult} to respond to the caller with the given results. If those 227 * functions return without calling {@link #sendResult}, they must instead call 228 * {@link #detach} before returning, and then may call {@link #sendResult} when 229 * they are done. If more than one of those methods is called, an exception will 230 * be thrown. 231 * 232 * @see MediaBrowserServiceCompat#onLoadChildren 233 * @see MediaBrowserServiceCompat#onLoadItem 234 */ 235 public static class Result<T> { 236 private Object mDebug; 237 private boolean mDetachCalled; 238 private boolean mSendResultCalled; 239 private int mFlags; 240 241 Result(Object debug) { 242 mDebug = debug; 243 } 244 245 /** 246 * Send the result back to the caller. 247 */ 248 public void sendResult(T result) { 249 if (mSendResultCalled) { 250 throw new IllegalStateException("sendResult() called twice for: " + mDebug); 251 } 252 mSendResultCalled = true; 253 onResultSent(result, mFlags); 254 } 255 256 /** 257 * Detach this message from the current thread and allow the {@link #sendResult} 258 * call to happen later. 259 */ 260 public void detach() { 261 if (mDetachCalled) { 262 throw new IllegalStateException("detach() called when detach() had already" 263 + " been called for: " + mDebug); 264 } 265 if (mSendResultCalled) { 266 throw new IllegalStateException("detach() called when sendResult() had already" 267 + " been called for: " + mDebug); 268 } 269 mDetachCalled = true; 270 } 271 272 boolean isDone() { 273 return mDetachCalled || mSendResultCalled; 274 } 275 276 void setFlags(@ResultFlags int flags) { 277 mFlags = flags; 278 } 279 280 /** 281 * Called when the result is sent, after assertions about not being called twice 282 * have happened. 283 */ 284 void onResultSent(T result, @ResultFlags int flags) { 285 } 286 } 287 288 private class ServiceImpl { 289 public void connect(final String pkg, final int uid, final Bundle rootHints, 290 final ServiceCallbacks callbacks) { 291 292 if (!isValidPackage(pkg, uid)) { 293 throw new IllegalArgumentException("Package/uid mismatch: uid=" + uid 294 + " package=" + pkg); 295 } 296 297 mHandler.postOrRun(new Runnable() { 298 @Override 299 public void run() { 300 final IBinder b = callbacks.asBinder(); 301 302 // Clear out the old subscriptions. We are getting new ones. 303 mConnections.remove(b); 304 305 final ConnectionRecord connection = new ConnectionRecord(); 306 connection.pkg = pkg; 307 connection.rootHints = rootHints; 308 connection.callbacks = callbacks; 309 310 connection.root = 311 MediaBrowserServiceCompat.this.onGetRoot(pkg, uid, rootHints); 312 313 // If they didn't return something, don't allow this client. 314 if (connection.root == null) { 315 Log.i(TAG, "No root for client " + pkg + " from service " 316 + getClass().getName()); 317 try { 318 callbacks.onConnectFailed(); 319 } catch (RemoteException ex) { 320 Log.w(TAG, "Calling onConnectFailed() failed. Ignoring. " 321 + "pkg=" + pkg); 322 } 323 } else { 324 try { 325 mConnections.put(b, connection); 326 if (mSession != null) { 327 callbacks.onConnect(connection.root.getRootId(), 328 mSession, connection.root.getExtras()); 329 } 330 } catch (RemoteException ex) { 331 Log.w(TAG, "Calling onConnect() failed. Dropping client. " 332 + "pkg=" + pkg); 333 mConnections.remove(b); 334 } 335 } 336 } 337 }); 338 } 339 340 public void disconnect(final ServiceCallbacks callbacks) { 341 mHandler.postOrRun(new Runnable() { 342 @Override 343 public void run() { 344 final IBinder b = callbacks.asBinder(); 345 346 // Clear out the old subscriptions. We are getting new ones. 347 final ConnectionRecord old = mConnections.remove(b); 348 if (old != null) { 349 // TODO 350 } 351 } 352 }); 353 } 354 355 public void addSubscription(final String id, final Bundle options, 356 final ServiceCallbacks callbacks) { 357 mHandler.postOrRun(new Runnable() { 358 @Override 359 public void run() { 360 final IBinder b = callbacks.asBinder(); 361 362 // Get the record for the connection 363 final ConnectionRecord connection = mConnections.get(b); 364 if (connection == null) { 365 Log.w(TAG, "addSubscription for callback that isn't registered id=" 366 + id); 367 return; 368 } 369 370 MediaBrowserServiceCompat.this.addSubscription(id, connection, options); 371 } 372 }); 373 } 374 375 public void removeSubscription(final String id, final Bundle options, 376 final ServiceCallbacks callbacks) { 377 mHandler.postOrRun(new Runnable() { 378 @Override 379 public void run() { 380 final IBinder b = callbacks.asBinder(); 381 382 ConnectionRecord connection = mConnections.get(b); 383 if (connection == null) { 384 Log.w(TAG, "removeSubscription for callback that isn't registered id=" 385 + id); 386 return; 387 } 388 if (!MediaBrowserServiceCompat.this.removeSubscription( 389 id, connection, options)) { 390 Log.w(TAG, "removeSubscription called for " + id 391 + " which is not subscribed"); 392 } 393 } 394 }); 395 } 396 397 public void getMediaItem(final String mediaId, final ResultReceiver receiver) { 398 if (TextUtils.isEmpty(mediaId) || receiver == null) { 399 return; 400 } 401 402 mHandler.postOrRun(new Runnable() { 403 @Override 404 public void run() { 405 performLoadItem(mediaId, receiver); 406 } 407 }); 408 } 409 } 410 411 private class ServiceImplApi21 implements MediaBrowserServiceCompatApi21.ServiceImplApi21 { 412 final ServiceImpl mServiceImpl; 413 414 ServiceImplApi21() { 415 mServiceImpl = mHandler.getServiceImpl(); 416 } 417 418 @Override 419 public void connect(final String pkg, final Bundle rootHints, 420 final MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) { 421 mServiceImpl.connect(pkg, Binder.getCallingUid(), rootHints, 422 new ServiceCallbacksApi21(callbacks)); 423 } 424 425 @Override 426 public void disconnect(final MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) { 427 mServiceImpl.disconnect(new ServiceCallbacksApi21(callbacks)); 428 } 429 430 431 @Override 432 public void addSubscription( 433 final String id, final MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) { 434 mServiceImpl.addSubscription(id, null, new ServiceCallbacksApi21(callbacks)); 435 } 436 437 @Override 438 public void removeSubscription(final String id, 439 final MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) { 440 mServiceImpl.removeSubscription(id, null, new ServiceCallbacksApi21(callbacks)); 441 } 442 } 443 444 private class ServiceImplApi23 extends ServiceImplApi21 445 implements MediaBrowserServiceCompatApi23.ServiceImplApi23 { 446 @Override 447 public void getMediaItem(final String mediaId, 448 final MediaBrowserServiceCompatApi23.ItemCallback cb) { 449 ResultReceiver receiverCompat = new ResultReceiver(mHandler) { 450 @Override 451 protected void onReceiveResult(int resultCode, Bundle resultData) { 452 MediaBrowserCompat.MediaItem item = resultData.getParcelable(KEY_MEDIA_ITEM); 453 Parcel itemParcel = null; 454 if (item != null) { 455 itemParcel = Parcel.obtain(); 456 item.writeToParcel(itemParcel, 0); 457 } 458 cb.onItemLoaded(resultCode, resultData, itemParcel); 459 } 460 }; 461 mServiceImpl.getMediaItem(mediaId, receiverCompat); 462 } 463 } 464 465 private interface ServiceCallbacks { 466 IBinder asBinder(); 467 void onConnect(String root, MediaSessionCompat.Token session, Bundle extras) 468 throws RemoteException; 469 void onConnectFailed() throws RemoteException; 470 void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, Bundle options) 471 throws RemoteException; 472 } 473 474 private class ServiceCallbacksCompat implements ServiceCallbacks { 475 final Messenger mCallbacks; 476 477 ServiceCallbacksCompat(Messenger callbacks) { 478 mCallbacks = callbacks; 479 } 480 481 public IBinder asBinder() { 482 return mCallbacks.getBinder(); 483 } 484 485 public void onConnect(String root, MediaSessionCompat.Token session, Bundle extras) 486 throws RemoteException { 487 if (extras == null) { 488 extras = new Bundle(); 489 } 490 extras.putInt(EXTRA_SERVICE_VERSION, SERVICE_VERSION_CURRENT); 491 Bundle data = new Bundle(); 492 data.putString(DATA_MEDIA_ITEM_ID, root); 493 data.putParcelable(DATA_MEDIA_SESSION_TOKEN, session); 494 data.putBundle(DATA_ROOT_HINTS, extras); 495 sendRequest(SERVICE_MSG_ON_CONNECT, data); 496 } 497 498 public void onConnectFailed() throws RemoteException { 499 sendRequest(SERVICE_MSG_ON_CONNECT_FAILED, null); 500 } 501 502 public void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, 503 Bundle options) throws RemoteException { 504 Bundle data = new Bundle(); 505 data.putString(DATA_MEDIA_ITEM_ID, mediaId); 506 data.putBundle(DATA_OPTIONS, options); 507 if (list != null) { 508 data.putParcelableArrayList(DATA_MEDIA_ITEM_LIST, 509 list instanceof ArrayList ? (ArrayList) list : new ArrayList<>(list)); 510 } 511 sendRequest(SERVICE_MSG_ON_LOAD_CHILDREN, data); 512 } 513 514 private void sendRequest(int what, Bundle data) throws RemoteException { 515 Message msg = Message.obtain(); 516 msg.what = what; 517 msg.arg1 = SERVICE_VERSION_CURRENT; 518 msg.setData(data); 519 mCallbacks.send(msg); 520 } 521 } 522 523 private class ServiceCallbacksApi21 implements ServiceCallbacks { 524 final MediaBrowserServiceCompatApi21.ServiceCallbacks mCallbacks; 525 Messenger mMessenger; 526 527 ServiceCallbacksApi21(MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) { 528 mCallbacks = callbacks; 529 } 530 531 public IBinder asBinder() { 532 return mCallbacks.asBinder(); 533 } 534 535 public void onConnect(String root, MediaSessionCompat.Token session, Bundle extras) 536 throws RemoteException { 537 if (extras == null) { 538 extras = new Bundle(); 539 } 540 mMessenger = new Messenger(mHandler); 541 BundleCompat.putBinder(extras, EXTRA_MESSENGER_BINDER, mMessenger.getBinder()); 542 extras.putInt(EXTRA_SERVICE_VERSION, SERVICE_VERSION_CURRENT); 543 mCallbacks.onConnect(root, session.getToken(), extras); 544 } 545 546 public void onConnectFailed() throws RemoteException { 547 mCallbacks.onConnectFailed(); 548 } 549 550 public void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, 551 Bundle options) throws RemoteException { 552 List<Parcel> parcelList = null; 553 if (list != null) { 554 parcelList = new ArrayList<>(); 555 for (MediaBrowserCompat.MediaItem item : list) { 556 Parcel parcel = Parcel.obtain(); 557 item.writeToParcel(parcel, 0); 558 parcelList.add(parcel); 559 } 560 } 561 mCallbacks.onLoadChildren(mediaId, parcelList); 562 } 563 } 564 565 @Override 566 public void onCreate() { 567 super.onCreate(); 568 if (Build.VERSION.SDK_INT >= 23) { 569 mImpl = new MediaBrowserServiceImplApi23(); 570 } else if (Build.VERSION.SDK_INT >= 21) { 571 mImpl = new MediaBrowserServiceImplApi21(); 572 } else { 573 mImpl = new MediaBrowserServiceImplBase(); 574 } 575 mImpl.onCreate(); 576 } 577 578 @Override 579 public IBinder onBind(Intent intent) { 580 return mImpl.onBind(intent); 581 } 582 583 @Override 584 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 585 } 586 587 /** 588 * Called to get the root information for browsing by a particular client. 589 * <p> 590 * The implementation should verify that the client package has permission 591 * to access browse media information before returning the root id; it 592 * should return null if the client is not allowed to access this 593 * information. 594 * </p> 595 * 596 * @param clientPackageName The package name of the application which is 597 * requesting access to browse media. 598 * @param clientUid The uid of the application which is requesting access to 599 * browse media. 600 * @param rootHints An optional bundle of service-specific arguments to send 601 * to the media browse service when connecting and retrieving the 602 * root id for browsing, or null if none. The contents of this 603 * bundle may affect the information returned when browsing. 604 * @return The {@link BrowserRoot} for accessing this app's content or null. 605 */ 606 public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName, 607 int clientUid, @Nullable Bundle rootHints); 608 609 /** 610 * Called to get information about the children of a media item. 611 * <p> 612 * Implementations must call {@link Result#sendResult result.sendResult} 613 * with the list of children. If loading the children will be an expensive 614 * operation that should be performed on another thread, 615 * {@link Result#detach result.detach} may be called before returning from 616 * this function, and then {@link Result#sendResult result.sendResult} 617 * called when the loading is complete. 618 * 619 * @param parentId The id of the parent media item whose children are to be 620 * queried. 621 * @param result The Result to send the list of children to, or null if the 622 * id is invalid. 623 */ 624 public abstract void onLoadChildren(@NonNull String parentId, 625 @NonNull Result<List<MediaBrowserCompat.MediaItem>> result); 626 627 /** 628 * Called to get information about the children of a media item. 629 * <p> 630 * Implementations must call {@link Result#sendResult result.sendResult} 631 * with the list of children. If loading the children will be an expensive 632 * operation that should be performed on another thread, 633 * {@link Result#detach result.detach} may be called before returning from 634 * this function, and then {@link Result#sendResult result.sendResult} 635 * called when the loading is complete. 636 * 637 * @param parentId The id of the parent media item whose children are to be 638 * queried. 639 * @param result The Result to send the list of children to, or null if the 640 * id is invalid. 641 * @param options A bundle of service-specific arguments sent from the media 642 * browse. The information returned through the result should be 643 * affected by the contents of this bundle. 644 * {@hide} 645 */ 646 public void onLoadChildren(@NonNull String parentId, 647 @NonNull Result<List<MediaBrowserCompat.MediaItem>> result, @NonNull Bundle options) { 648 // To support backward compatibility, when the implementation of MediaBrowserService doesn't 649 // override onLoadChildren() with options, onLoadChildren() without options will be used 650 // instead, and the options will be applied in the implementation of result.onResultSent(). 651 result.setFlags(RESULT_FLAG_OPTION_NOT_HANDLED); 652 onLoadChildren(parentId, result); 653 } 654 655 /** 656 * Called to get information about a specific media item. 657 * <p> 658 * Implementations must call {@link Result#sendResult result.sendResult}. If 659 * loading the item will be an expensive operation {@link Result#detach 660 * result.detach} may be called before returning from this function, and 661 * then {@link Result#sendResult result.sendResult} called when the item has 662 * been loaded. 663 * <p> 664 * The default implementation sends a null result. 665 * 666 * @param itemId The id for the specific 667 * {@link MediaBrowserCompat.MediaItem}. 668 * @param result The Result to send the item to, or null if the id is 669 * invalid. 670 */ 671 public void onLoadItem(String itemId, Result<MediaBrowserCompat.MediaItem> result) { 672 result.sendResult(null); 673 } 674 675 /** 676 * Call to set the media session. 677 * <p> 678 * This should be called as soon as possible during the service's startup. 679 * It may only be called once. 680 * 681 * @param token The token for the service's {@link MediaSessionCompat}. 682 */ 683 public void setSessionToken(final MediaSessionCompat.Token token) { 684 if (token == null) { 685 throw new IllegalArgumentException("Session token may not be null."); 686 } 687 if (mSession != null) { 688 throw new IllegalStateException("The session token has already been set."); 689 } 690 mSession = token; 691 mHandler.post(new Runnable() { 692 @Override 693 public void run() { 694 for (IBinder key : mConnections.keySet()) { 695 ConnectionRecord connection = mConnections.get(key); 696 try { 697 connection.callbacks.onConnect(connection.root.getRootId(), token, 698 connection.root.getExtras()); 699 } catch (RemoteException e) { 700 Log.w(TAG, "Connection for " + connection.pkg + " is no longer valid."); 701 mConnections.remove(key); 702 } 703 } 704 } 705 }); 706 } 707 708 /** 709 * Gets the session token, or null if it has not yet been created 710 * or if it has been destroyed. 711 */ 712 public @Nullable MediaSessionCompat.Token getSessionToken() { 713 return mSession; 714 } 715 716 /** 717 * Notifies all connected media browsers that the children of 718 * the specified parent id have changed in some way. 719 * This will cause browsers to fetch subscribed content again. 720 * 721 * @param parentId The id of the parent media item whose 722 * children changed. 723 */ 724 public void notifyChildrenChanged(@NonNull String parentId) { 725 notifyChildrenChangedInternal(parentId, null); 726 } 727 728 /** 729 * Notifies all connected media browsers that the children of 730 * the specified parent id have changed in some way. 731 * This will cause browsers to fetch subscribed content again. 732 * 733 * @param parentId The id of the parent media item whose 734 * children changed. 735 * @param options A bundle of service-specific arguments to send 736 * to the media browse. The contents of this bundle may 737 * contain the information about the change. 738 * {@hide} 739 */ 740 public void notifyChildrenChanged(@NonNull String parentId, @NonNull Bundle options) { 741 if (options == null) { 742 throw new IllegalArgumentException("options cannot be null in notifyChildrenChanged"); 743 } 744 notifyChildrenChangedInternal(parentId, options); 745 } 746 747 private void notifyChildrenChangedInternal(final String parentId, final Bundle options) { 748 if (parentId == null) { 749 throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged"); 750 } 751 mHandler.post(new Runnable() { 752 @Override 753 public void run() { 754 for (IBinder binder : mConnections.keySet()) { 755 ConnectionRecord connection = mConnections.get(binder); 756 List<Bundle> optionsList = connection.subscriptions.get(parentId); 757 if (optionsList != null) { 758 for (Bundle bundle : optionsList) { 759 if (MediaBrowserCompatUtils.hasDuplicatedItems(options, bundle)) { 760 performLoadChildren(parentId, connection, bundle); 761 break; 762 } 763 } 764 } 765 } 766 } 767 }); 768 } 769 770 /** 771 * Return whether the given package is one of the ones that is owned by the uid. 772 */ 773 private boolean isValidPackage(String pkg, int uid) { 774 if (pkg == null) { 775 return false; 776 } 777 final PackageManager pm = getPackageManager(); 778 final String[] packages = pm.getPackagesForUid(uid); 779 final int N = packages.length; 780 for (int i=0; i<N; i++) { 781 if (packages[i].equals(pkg)) { 782 return true; 783 } 784 } 785 return false; 786 } 787 788 /** 789 * Save the subscription and if it is a new subscription send the results. 790 */ 791 private void addSubscription(String id, ConnectionRecord connection, Bundle options) { 792 // Save the subscription 793 List<Bundle> optionsList = connection.subscriptions.get(id); 794 if (optionsList == null) { 795 optionsList = new ArrayList(); 796 } 797 for (Bundle bundle : optionsList) { 798 if (MediaBrowserCompatUtils.areSameOptions(options, bundle)) { 799 return; 800 } 801 } 802 optionsList.add(options); 803 connection.subscriptions.put(id, optionsList); 804 // send the results 805 performLoadChildren(id, connection, options); 806 } 807 808 /** 809 * Remove the subscription. 810 */ 811 private boolean removeSubscription(String id, ConnectionRecord connection, Bundle options) { 812 boolean removed = false; 813 List<Bundle> optionsList = connection.subscriptions.get(id); 814 if (optionsList != null) { 815 for (Bundle bundle : optionsList) { 816 if (MediaBrowserCompatUtils.areSameOptions(options, bundle)) { 817 removed = true; 818 optionsList.remove(bundle); 819 break; 820 } 821 } 822 if (optionsList.size() == 0) { 823 connection.subscriptions.remove(id); 824 } 825 } 826 return removed; 827 } 828 829 /** 830 * Call onLoadChildren and then send the results back to the connection. 831 * <p> 832 * Callers must make sure that this connection is still connected. 833 */ 834 private void performLoadChildren(final String parentId, final ConnectionRecord connection, 835 final Bundle options) { 836 final Result<List<MediaBrowserCompat.MediaItem>> result 837 = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) { 838 @Override 839 void onResultSent(List<MediaBrowserCompat.MediaItem> list, @ResultFlags int flag) { 840 if (mConnections.get(connection.callbacks.asBinder()) != connection) { 841 if (DBG) { 842 Log.d(TAG, "Not sending onLoadChildren result for connection that has" 843 + " been disconnected. pkg=" + connection.pkg + " id=" + parentId); 844 } 845 return; 846 } 847 848 List<MediaBrowserCompat.MediaItem> filteredList = 849 (flag & RESULT_FLAG_OPTION_NOT_HANDLED) != 0 850 ? applyOptions(list, options) : list; 851 try { 852 connection.callbacks.onLoadChildren(parentId, filteredList, options); 853 } catch (RemoteException ex) { 854 // The other side is in the process of crashing. 855 Log.w(TAG, "Calling onLoadChildren() failed for id=" + parentId 856 + " package=" + connection.pkg); 857 } 858 } 859 }; 860 861 if (options == null) { 862 onLoadChildren(parentId, result); 863 } else { 864 onLoadChildren(parentId, result, options); 865 } 866 867 if (!result.isDone()) { 868 throw new IllegalStateException("onLoadChildren must call detach() or sendResult()" 869 + " before returning for package=" + connection.pkg + " id=" + parentId); 870 } 871 } 872 873 private List<MediaBrowserCompat.MediaItem> applyOptions(List<MediaBrowserCompat.MediaItem> list, 874 final Bundle options) { 875 int page = options.getInt(MediaBrowserCompat.EXTRA_PAGE, -1); 876 int pageSize = options.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, -1); 877 if (page == -1 && pageSize == -1) { 878 return list; 879 } 880 int fromIndex = pageSize * (page - 1); 881 int toIndex = fromIndex + pageSize; 882 if (page < 1 || pageSize < 1 || fromIndex >= list.size()) { 883 return Collections.emptyList(); 884 } 885 if (toIndex > list.size()) { 886 toIndex = list.size(); 887 } 888 return list.subList(fromIndex, toIndex); 889 } 890 891 private void performLoadItem(String itemId, final ResultReceiver receiver) { 892 final Result<MediaBrowserCompat.MediaItem> result = 893 new Result<MediaBrowserCompat.MediaItem>(itemId) { 894 @Override 895 void onResultSent(MediaBrowserCompat.MediaItem item, @ResultFlags int flag) { 896 Bundle bundle = new Bundle(); 897 bundle.putParcelable(KEY_MEDIA_ITEM, item); 898 receiver.send(0, bundle); 899 } 900 }; 901 902 MediaBrowserServiceCompat.this.onLoadItem(itemId, result); 903 904 if (!result.isDone()) { 905 throw new IllegalStateException("onLoadItem must call detach() or sendResult()" 906 + " before returning for id=" + itemId); 907 } 908 } 909 910 /** 911 * Contains information that the browser service needs to send to the client 912 * when first connected. 913 */ 914 public static final class BrowserRoot { 915 final private String mRootId; 916 final private Bundle mExtras; 917 918 /** 919 * Constructs a browser root. 920 * @param rootId The root id for browsing. 921 * @param extras Any extras about the browser service. 922 */ 923 public BrowserRoot(@NonNull String rootId, @Nullable Bundle extras) { 924 if (rootId == null) { 925 throw new IllegalArgumentException("The root id in BrowserRoot cannot be null. " + 926 "Use null for BrowserRoot instead."); 927 } 928 mRootId = rootId; 929 mExtras = extras; 930 } 931 932 /** 933 * Gets the root id for browsing. 934 */ 935 public String getRootId() { 936 return mRootId; 937 } 938 939 /** 940 * Gets any extras about the browser service. 941 */ 942 public Bundle getExtras() { 943 return mExtras; 944 } 945 } 946} 947