MediaBrowserServiceCompat.java revision 05ef0bb8aa7f86fea5ab65c54237eae1d3973588
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 case CLIENT_MSG_REGISTER_CALLBACK_MESSENGER: 182 mServiceImpl.registerCallbacks(new ServiceCallbacksCompat(msg.replyTo)); 183 break; 184 default: 185 Log.w(TAG, "Unhandled message: " + msg 186 + "\n Service version: " + SERVICE_VERSION_CURRENT 187 + "\n Client version: " + msg.arg1); 188 } 189 } 190 191 @Override 192 public boolean sendMessageAtTime(Message msg, long uptimeMillis) { 193 // Binder.getCallingUid() in handleMessage will return the uid of this process. 194 // In order to get the right calling uid, Binder.getCallingUid() should be called here. 195 Bundle data = msg.getData(); 196 data.setClassLoader(MediaBrowserCompat.class.getClassLoader()); 197 data.putInt(DATA_CALLING_UID, Binder.getCallingUid()); 198 return super.sendMessageAtTime(msg, uptimeMillis); 199 } 200 201 public void postOrRun(Runnable r) { 202 if (Thread.currentThread() == getLooper().getThread()) { 203 r.run(); 204 } else { 205 post(r); 206 } 207 } 208 209 public ServiceImpl getServiceImpl() { 210 return mServiceImpl; 211 } 212 } 213 214 /** 215 * All the info about a connection. 216 */ 217 private class ConnectionRecord { 218 String pkg; 219 Bundle rootHints; 220 ServiceCallbacks callbacks; 221 BrowserRoot root; 222 HashMap<String, List<Bundle>> subscriptions = new HashMap(); 223 } 224 225 /** 226 * Completion handler for asynchronous callback methods in {@link MediaBrowserServiceCompat}. 227 * <p> 228 * Each of the methods that takes one of these to send the result must call 229 * {@link #sendResult} to respond to the caller with the given results. If those 230 * functions return without calling {@link #sendResult}, they must instead call 231 * {@link #detach} before returning, and then may call {@link #sendResult} when 232 * they are done. If more than one of those methods is called, an exception will 233 * be thrown. 234 * 235 * @see MediaBrowserServiceCompat#onLoadChildren 236 * @see MediaBrowserServiceCompat#onLoadItem 237 */ 238 public static class Result<T> { 239 private Object mDebug; 240 private boolean mDetachCalled; 241 private boolean mSendResultCalled; 242 private int mFlags; 243 244 Result(Object debug) { 245 mDebug = debug; 246 } 247 248 /** 249 * Send the result back to the caller. 250 */ 251 public void sendResult(T result) { 252 if (mSendResultCalled) { 253 throw new IllegalStateException("sendResult() called twice for: " + mDebug); 254 } 255 mSendResultCalled = true; 256 onResultSent(result, mFlags); 257 } 258 259 /** 260 * Detach this message from the current thread and allow the {@link #sendResult} 261 * call to happen later. 262 */ 263 public void detach() { 264 if (mDetachCalled) { 265 throw new IllegalStateException("detach() called when detach() had already" 266 + " been called for: " + mDebug); 267 } 268 if (mSendResultCalled) { 269 throw new IllegalStateException("detach() called when sendResult() had already" 270 + " been called for: " + mDebug); 271 } 272 mDetachCalled = true; 273 } 274 275 boolean isDone() { 276 return mDetachCalled || mSendResultCalled; 277 } 278 279 void setFlags(@ResultFlags int flags) { 280 mFlags = flags; 281 } 282 283 /** 284 * Called when the result is sent, after assertions about not being called twice 285 * have happened. 286 */ 287 void onResultSent(T result, @ResultFlags int flags) { 288 } 289 } 290 291 private class ServiceImpl { 292 public void connect(final String pkg, final int uid, final Bundle rootHints, 293 final ServiceCallbacks callbacks) { 294 295 if (!isValidPackage(pkg, uid)) { 296 throw new IllegalArgumentException("Package/uid mismatch: uid=" + uid 297 + " package=" + pkg); 298 } 299 300 mHandler.postOrRun(new Runnable() { 301 @Override 302 public void run() { 303 final IBinder b = callbacks.asBinder(); 304 305 // Clear out the old subscriptions. We are getting new ones. 306 mConnections.remove(b); 307 308 final ConnectionRecord connection = new ConnectionRecord(); 309 connection.pkg = pkg; 310 connection.rootHints = rootHints; 311 connection.callbacks = callbacks; 312 313 connection.root = 314 MediaBrowserServiceCompat.this.onGetRoot(pkg, uid, rootHints); 315 316 // If they didn't return something, don't allow this client. 317 if (connection.root == null) { 318 Log.i(TAG, "No root for client " + pkg + " from service " 319 + getClass().getName()); 320 try { 321 callbacks.onConnectFailed(); 322 } catch (RemoteException ex) { 323 Log.w(TAG, "Calling onConnectFailed() failed. Ignoring. " 324 + "pkg=" + pkg); 325 } 326 } else { 327 try { 328 mConnections.put(b, connection); 329 if (mSession != null) { 330 callbacks.onConnect(connection.root.getRootId(), 331 mSession, connection.root.getExtras()); 332 } 333 } catch (RemoteException ex) { 334 Log.w(TAG, "Calling onConnect() failed. Dropping client. " 335 + "pkg=" + pkg); 336 mConnections.remove(b); 337 } 338 } 339 } 340 }); 341 } 342 343 public void disconnect(final ServiceCallbacks callbacks) { 344 mHandler.postOrRun(new Runnable() { 345 @Override 346 public void run() { 347 final IBinder b = callbacks.asBinder(); 348 349 // Clear out the old subscriptions. We are getting new ones. 350 final ConnectionRecord old = mConnections.remove(b); 351 if (old != null) { 352 // TODO 353 } 354 } 355 }); 356 } 357 358 public void addSubscription(final String id, final Bundle options, 359 final ServiceCallbacks callbacks) { 360 mHandler.postOrRun(new Runnable() { 361 @Override 362 public void run() { 363 final IBinder b = callbacks.asBinder(); 364 365 // Get the record for the connection 366 final ConnectionRecord connection = mConnections.get(b); 367 if (connection == null) { 368 Log.w(TAG, "addSubscription for callback that isn't registered id=" 369 + id); 370 return; 371 } 372 373 MediaBrowserServiceCompat.this.addSubscription(id, connection, options); 374 } 375 }); 376 } 377 378 public void removeSubscription(final String id, final Bundle options, 379 final ServiceCallbacks callbacks) { 380 mHandler.postOrRun(new Runnable() { 381 @Override 382 public void run() { 383 final IBinder b = callbacks.asBinder(); 384 385 ConnectionRecord connection = mConnections.get(b); 386 if (connection == null) { 387 Log.w(TAG, "removeSubscription for callback that isn't registered id=" 388 + id); 389 return; 390 } 391 if (!MediaBrowserServiceCompat.this.removeSubscription( 392 id, connection, options)) { 393 Log.w(TAG, "removeSubscription called for " + id 394 + " which is not subscribed"); 395 } 396 } 397 }); 398 } 399 400 public void getMediaItem(final String mediaId, final ResultReceiver receiver) { 401 if (TextUtils.isEmpty(mediaId) || receiver == null) { 402 return; 403 } 404 405 mHandler.postOrRun(new Runnable() { 406 @Override 407 public void run() { 408 performLoadItem(mediaId, receiver); 409 } 410 }); 411 } 412 413 // Used when {@link MediaBrowserProtocol#EXTRA_MESSENGER_BINDER} is used. 414 public void registerCallbacks(final ServiceCallbacks callbacks) { 415 mHandler.postOrRun(new Runnable() { 416 @Override 417 public void run() { 418 final IBinder b = callbacks.asBinder(); 419 // Clear out the old subscriptions. We are getting new ones. 420 mConnections.remove(b); 421 422 final ConnectionRecord connection = new ConnectionRecord(); 423 connection.callbacks = callbacks; 424 mConnections.put(b, connection); 425 } 426 }); 427 } 428 } 429 430 private class ServiceImplApi21 implements MediaBrowserServiceCompatApi21.ServiceImplApi21 { 431 final ServiceImpl mServiceImpl; 432 433 ServiceImplApi21() { 434 mServiceImpl = mHandler.getServiceImpl(); 435 } 436 437 @Override 438 public void connect(final String pkg, final Bundle rootHints, 439 final MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) { 440 mServiceImpl.connect(pkg, Binder.getCallingUid(), rootHints, 441 new ServiceCallbacksApi21(callbacks)); 442 } 443 444 @Override 445 public void disconnect(final MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) { 446 mServiceImpl.disconnect(new ServiceCallbacksApi21(callbacks)); 447 } 448 449 450 @Override 451 public void addSubscription( 452 final String id, final MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) { 453 mServiceImpl.addSubscription(id, null, new ServiceCallbacksApi21(callbacks)); 454 } 455 456 @Override 457 public void removeSubscription(final String id, 458 final MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) { 459 mServiceImpl.removeSubscription(id, null, new ServiceCallbacksApi21(callbacks)); 460 } 461 } 462 463 private class ServiceImplApi23 extends ServiceImplApi21 464 implements MediaBrowserServiceCompatApi23.ServiceImplApi23 { 465 @Override 466 public void getMediaItem(final String mediaId, 467 final MediaBrowserServiceCompatApi23.ItemCallback cb) { 468 ResultReceiver receiverCompat = new ResultReceiver(mHandler) { 469 @Override 470 protected void onReceiveResult(int resultCode, Bundle resultData) { 471 MediaBrowserCompat.MediaItem item = resultData.getParcelable(KEY_MEDIA_ITEM); 472 Parcel itemParcel = null; 473 if (item != null) { 474 itemParcel = Parcel.obtain(); 475 item.writeToParcel(itemParcel, 0); 476 } 477 cb.onItemLoaded(resultCode, resultData, itemParcel); 478 } 479 }; 480 mServiceImpl.getMediaItem(mediaId, receiverCompat); 481 } 482 } 483 484 private interface ServiceCallbacks { 485 IBinder asBinder(); 486 void onConnect(String root, MediaSessionCompat.Token session, Bundle extras) 487 throws RemoteException; 488 void onConnectFailed() throws RemoteException; 489 void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, Bundle options) 490 throws RemoteException; 491 } 492 493 private class ServiceCallbacksCompat implements ServiceCallbacks { 494 final Messenger mCallbacks; 495 496 ServiceCallbacksCompat(Messenger callbacks) { 497 mCallbacks = callbacks; 498 } 499 500 public IBinder asBinder() { 501 return mCallbacks.getBinder(); 502 } 503 504 public void onConnect(String root, MediaSessionCompat.Token session, Bundle extras) 505 throws RemoteException { 506 if (extras == null) { 507 extras = new Bundle(); 508 } 509 extras.putInt(EXTRA_SERVICE_VERSION, SERVICE_VERSION_CURRENT); 510 Bundle data = new Bundle(); 511 data.putString(DATA_MEDIA_ITEM_ID, root); 512 data.putParcelable(DATA_MEDIA_SESSION_TOKEN, session); 513 data.putBundle(DATA_ROOT_HINTS, extras); 514 sendRequest(SERVICE_MSG_ON_CONNECT, data); 515 } 516 517 public void onConnectFailed() throws RemoteException { 518 sendRequest(SERVICE_MSG_ON_CONNECT_FAILED, null); 519 } 520 521 public void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, 522 Bundle options) throws RemoteException { 523 Bundle data = new Bundle(); 524 data.putString(DATA_MEDIA_ITEM_ID, mediaId); 525 data.putBundle(DATA_OPTIONS, options); 526 if (list != null) { 527 data.putParcelableArrayList(DATA_MEDIA_ITEM_LIST, 528 list instanceof ArrayList ? (ArrayList) list : new ArrayList<>(list)); 529 } 530 sendRequest(SERVICE_MSG_ON_LOAD_CHILDREN, data); 531 } 532 533 private void sendRequest(int what, Bundle data) throws RemoteException { 534 Message msg = Message.obtain(); 535 msg.what = what; 536 msg.arg1 = SERVICE_VERSION_CURRENT; 537 msg.setData(data); 538 mCallbacks.send(msg); 539 } 540 } 541 542 private class ServiceCallbacksApi21 implements ServiceCallbacks { 543 final MediaBrowserServiceCompatApi21.ServiceCallbacks mCallbacks; 544 Messenger mMessenger; 545 546 ServiceCallbacksApi21(MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) { 547 mCallbacks = callbacks; 548 } 549 550 public IBinder asBinder() { 551 return mCallbacks.asBinder(); 552 } 553 554 public void onConnect(String root, MediaSessionCompat.Token session, Bundle extras) 555 throws RemoteException { 556 if (extras == null) { 557 extras = new Bundle(); 558 } 559 mMessenger = new Messenger(mHandler); 560 BundleCompat.putBinder(extras, EXTRA_MESSENGER_BINDER, mMessenger.getBinder()); 561 extras.putInt(EXTRA_SERVICE_VERSION, SERVICE_VERSION_CURRENT); 562 mCallbacks.onConnect(root, session.getToken(), extras); 563 } 564 565 public void onConnectFailed() throws RemoteException { 566 mCallbacks.onConnectFailed(); 567 } 568 569 public void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, 570 Bundle options) throws RemoteException { 571 List<Parcel> parcelList = null; 572 if (list != null) { 573 parcelList = new ArrayList<>(); 574 for (MediaBrowserCompat.MediaItem item : list) { 575 Parcel parcel = Parcel.obtain(); 576 item.writeToParcel(parcel, 0); 577 parcelList.add(parcel); 578 } 579 } 580 mCallbacks.onLoadChildren(mediaId, parcelList); 581 } 582 } 583 584 @Override 585 public void onCreate() { 586 super.onCreate(); 587 if (Build.VERSION.SDK_INT >= 23) { 588 mImpl = new MediaBrowserServiceImplApi23(); 589 } else if (Build.VERSION.SDK_INT >= 21) { 590 mImpl = new MediaBrowserServiceImplApi21(); 591 } else { 592 mImpl = new MediaBrowserServiceImplBase(); 593 } 594 mImpl.onCreate(); 595 } 596 597 @Override 598 public IBinder onBind(Intent intent) { 599 return mImpl.onBind(intent); 600 } 601 602 @Override 603 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 604 } 605 606 /** 607 * Called to get the root information for browsing by a particular client. 608 * <p> 609 * The implementation should verify that the client package has permission 610 * to access browse media information before returning the root id; it 611 * should return null if the client is not allowed to access this 612 * information. 613 * </p> 614 * 615 * @param clientPackageName The package name of the application which is 616 * requesting access to browse media. 617 * @param clientUid The uid of the application which is requesting access to 618 * browse media. 619 * @param rootHints An optional bundle of service-specific arguments to send 620 * to the media browse service when connecting and retrieving the 621 * root id for browsing, or null if none. The contents of this 622 * bundle may affect the information returned when browsing. 623 * @return The {@link BrowserRoot} for accessing this app's content or null. 624 */ 625 public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName, 626 int clientUid, @Nullable Bundle rootHints); 627 628 /** 629 * Called to get information about the children of a media item. 630 * <p> 631 * Implementations must call {@link Result#sendResult result.sendResult} 632 * with the list of children. If loading the children will be an expensive 633 * operation that should be performed on another thread, 634 * {@link Result#detach result.detach} may be called before returning from 635 * this function, and then {@link Result#sendResult result.sendResult} 636 * called when the loading is complete. 637 * 638 * @param parentId The id of the parent media item whose children are to be 639 * queried. 640 * @param result The Result to send the list of children to, or null if the 641 * id is invalid. 642 */ 643 public abstract void onLoadChildren(@NonNull String parentId, 644 @NonNull Result<List<MediaBrowserCompat.MediaItem>> result); 645 646 /** 647 * Called to get information about the children of a media item. 648 * <p> 649 * Implementations must call {@link Result#sendResult result.sendResult} 650 * with the list of children. If loading the children will be an expensive 651 * operation that should be performed on another thread, 652 * {@link Result#detach result.detach} may be called before returning from 653 * this function, and then {@link Result#sendResult result.sendResult} 654 * called when the loading is complete. 655 * 656 * @param parentId The id of the parent media item whose children are to be 657 * queried. 658 * @param result The Result to send the list of children to, or null if the 659 * id is invalid. 660 * @param options A bundle of service-specific arguments sent from the media 661 * browse. The information returned through the result should be 662 * affected by the contents of this bundle. 663 * {@hide} 664 */ 665 public void onLoadChildren(@NonNull String parentId, 666 @NonNull Result<List<MediaBrowserCompat.MediaItem>> result, @NonNull Bundle options) { 667 // To support backward compatibility, when the implementation of MediaBrowserService doesn't 668 // override onLoadChildren() with options, onLoadChildren() without options will be used 669 // instead, and the options will be applied in the implementation of result.onResultSent(). 670 result.setFlags(RESULT_FLAG_OPTION_NOT_HANDLED); 671 onLoadChildren(parentId, result); 672 } 673 674 /** 675 * Called to get information about a specific media item. 676 * <p> 677 * Implementations must call {@link Result#sendResult result.sendResult}. If 678 * loading the item will be an expensive operation {@link Result#detach 679 * result.detach} may be called before returning from this function, and 680 * then {@link Result#sendResult result.sendResult} called when the item has 681 * been loaded. 682 * <p> 683 * The default implementation sends a null result. 684 * 685 * @param itemId The id for the specific 686 * {@link MediaBrowserCompat.MediaItem}. 687 * @param result The Result to send the item to, or null if the id is 688 * invalid. 689 */ 690 public void onLoadItem(String itemId, Result<MediaBrowserCompat.MediaItem> result) { 691 result.sendResult(null); 692 } 693 694 /** 695 * Call to set the media session. 696 * <p> 697 * This should be called as soon as possible during the service's startup. 698 * It may only be called once. 699 * 700 * @param token The token for the service's {@link MediaSessionCompat}. 701 */ 702 public void setSessionToken(final MediaSessionCompat.Token token) { 703 if (token == null) { 704 throw new IllegalArgumentException("Session token may not be null."); 705 } 706 if (mSession != null) { 707 throw new IllegalStateException("The session token has already been set."); 708 } 709 mSession = token; 710 mHandler.post(new Runnable() { 711 @Override 712 public void run() { 713 for (IBinder key : mConnections.keySet()) { 714 ConnectionRecord connection = mConnections.get(key); 715 try { 716 connection.callbacks.onConnect(connection.root.getRootId(), token, 717 connection.root.getExtras()); 718 } catch (RemoteException e) { 719 Log.w(TAG, "Connection for " + connection.pkg + " is no longer valid."); 720 mConnections.remove(key); 721 } 722 } 723 } 724 }); 725 } 726 727 /** 728 * Gets the session token, or null if it has not yet been created 729 * or if it has been destroyed. 730 */ 731 public @Nullable MediaSessionCompat.Token getSessionToken() { 732 return mSession; 733 } 734 735 /** 736 * Notifies all connected media browsers that the children of 737 * the specified parent id have changed in some way. 738 * This will cause browsers to fetch subscribed content again. 739 * 740 * @param parentId The id of the parent media item whose 741 * children changed. 742 */ 743 public void notifyChildrenChanged(@NonNull String parentId) { 744 notifyChildrenChangedInternal(parentId, null); 745 } 746 747 /** 748 * Notifies all connected media browsers that the children of 749 * the specified parent id have changed in some way. 750 * This will cause browsers to fetch subscribed content again. 751 * 752 * @param parentId The id of the parent media item whose 753 * children changed. 754 * @param options A bundle of service-specific arguments to send 755 * to the media browse. The contents of this bundle may 756 * contain the information about the change. 757 * {@hide} 758 */ 759 public void notifyChildrenChanged(@NonNull String parentId, @NonNull Bundle options) { 760 if (options == null) { 761 throw new IllegalArgumentException("options cannot be null in notifyChildrenChanged"); 762 } 763 notifyChildrenChangedInternal(parentId, options); 764 } 765 766 private void notifyChildrenChangedInternal(final String parentId, final Bundle options) { 767 if (parentId == null) { 768 throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged"); 769 } 770 mHandler.post(new Runnable() { 771 @Override 772 public void run() { 773 for (IBinder binder : mConnections.keySet()) { 774 ConnectionRecord connection = mConnections.get(binder); 775 List<Bundle> optionsList = connection.subscriptions.get(parentId); 776 if (optionsList != null) { 777 for (Bundle bundle : optionsList) { 778 if (MediaBrowserCompatUtils.hasDuplicatedItems(options, bundle)) { 779 performLoadChildren(parentId, connection, bundle); 780 break; 781 } 782 } 783 } 784 } 785 } 786 }); 787 } 788 789 /** 790 * Return whether the given package is one of the ones that is owned by the uid. 791 */ 792 private boolean isValidPackage(String pkg, int uid) { 793 if (pkg == null) { 794 return false; 795 } 796 final PackageManager pm = getPackageManager(); 797 final String[] packages = pm.getPackagesForUid(uid); 798 final int N = packages.length; 799 for (int i=0; i<N; i++) { 800 if (packages[i].equals(pkg)) { 801 return true; 802 } 803 } 804 return false; 805 } 806 807 /** 808 * Save the subscription and if it is a new subscription send the results. 809 */ 810 private void addSubscription(String id, ConnectionRecord connection, Bundle options) { 811 // Save the subscription 812 List<Bundle> optionsList = connection.subscriptions.get(id); 813 if (optionsList == null) { 814 optionsList = new ArrayList(); 815 } 816 for (Bundle bundle : optionsList) { 817 if (MediaBrowserCompatUtils.areSameOptions(options, bundle)) { 818 return; 819 } 820 } 821 optionsList.add(options); 822 connection.subscriptions.put(id, optionsList); 823 // send the results 824 performLoadChildren(id, connection, options); 825 } 826 827 /** 828 * Remove the subscription. 829 */ 830 private boolean removeSubscription(String id, ConnectionRecord connection, Bundle options) { 831 boolean removed = false; 832 List<Bundle> optionsList = connection.subscriptions.get(id); 833 if (optionsList != null) { 834 for (Bundle bundle : optionsList) { 835 if (MediaBrowserCompatUtils.areSameOptions(options, bundle)) { 836 removed = true; 837 optionsList.remove(bundle); 838 break; 839 } 840 } 841 if (optionsList.size() == 0) { 842 connection.subscriptions.remove(id); 843 } 844 } 845 return removed; 846 } 847 848 /** 849 * Call onLoadChildren and then send the results back to the connection. 850 * <p> 851 * Callers must make sure that this connection is still connected. 852 */ 853 private void performLoadChildren(final String parentId, final ConnectionRecord connection, 854 final Bundle options) { 855 final Result<List<MediaBrowserCompat.MediaItem>> result 856 = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) { 857 @Override 858 void onResultSent(List<MediaBrowserCompat.MediaItem> list, @ResultFlags int flag) { 859 if (mConnections.get(connection.callbacks.asBinder()) != connection) { 860 if (DBG) { 861 Log.d(TAG, "Not sending onLoadChildren result for connection that has" 862 + " been disconnected. pkg=" + connection.pkg + " id=" + parentId); 863 } 864 return; 865 } 866 867 List<MediaBrowserCompat.MediaItem> filteredList = 868 (flag & RESULT_FLAG_OPTION_NOT_HANDLED) != 0 869 ? MediaBrowserCompatUtils.applyOptions(list, options) : list; 870 try { 871 connection.callbacks.onLoadChildren(parentId, filteredList, options); 872 } catch (RemoteException ex) { 873 // The other side is in the process of crashing. 874 Log.w(TAG, "Calling onLoadChildren() failed for id=" + parentId 875 + " package=" + connection.pkg); 876 } 877 } 878 }; 879 880 if (options == null) { 881 onLoadChildren(parentId, result); 882 } else { 883 onLoadChildren(parentId, result, options); 884 } 885 886 if (!result.isDone()) { 887 throw new IllegalStateException("onLoadChildren must call detach() or sendResult()" 888 + " before returning for package=" + connection.pkg + " id=" + parentId); 889 } 890 } 891 892 private List<MediaBrowserCompat.MediaItem> applyOptions(List<MediaBrowserCompat.MediaItem> list, 893 final Bundle options) { 894 int page = options.getInt(MediaBrowserCompat.EXTRA_PAGE, -1); 895 int pageSize = options.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, -1); 896 if (page == -1 && pageSize == -1) { 897 return list; 898 } 899 int fromIndex = pageSize * (page - 1); 900 int toIndex = fromIndex + pageSize; 901 if (page < 1 || pageSize < 1 || fromIndex >= list.size()) { 902 return Collections.emptyList(); 903 } 904 if (toIndex > list.size()) { 905 toIndex = list.size(); 906 } 907 return list.subList(fromIndex, toIndex); 908 } 909 910 private void performLoadItem(String itemId, final ResultReceiver receiver) { 911 final Result<MediaBrowserCompat.MediaItem> result = 912 new Result<MediaBrowserCompat.MediaItem>(itemId) { 913 @Override 914 void onResultSent(MediaBrowserCompat.MediaItem item, @ResultFlags int flag) { 915 Bundle bundle = new Bundle(); 916 bundle.putParcelable(KEY_MEDIA_ITEM, item); 917 receiver.send(0, bundle); 918 } 919 }; 920 921 MediaBrowserServiceCompat.this.onLoadItem(itemId, result); 922 923 if (!result.isDone()) { 924 throw new IllegalStateException("onLoadItem must call detach() or sendResult()" 925 + " before returning for id=" + itemId); 926 } 927 } 928 929 /** 930 * Contains information that the browser service needs to send to the client 931 * when first connected. 932 */ 933 public static final class BrowserRoot { 934 /** 935 * The lookup key for a boolean that indicates whether the browser service should return a 936 * browser root for recently played media items. 937 * 938 * <p>When creating a media browser for a given media browser service, this key can be 939 * supplied as a root hint for retrieving media items that are recently played. 940 * If the media browser service can provide such media items, the implementation must return 941 * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back. 942 * 943 * <p>The root hint may contain multiple keys. 944 * 945 * @see #EXTRA_OFFLINE 946 * @see #EXTRA_SUGGESTED 947 * @hide 948 */ 949 public static final String EXTRA_RECENT = "android.service.media.extra.RECENT"; 950 951 /** 952 * The lookup key for a boolean that indicates whether the browser service should return a 953 * browser root for offline media items. 954 * 955 * <p>When creating a media browser for a given media browser service, this key can be 956 * supplied as a root hint for retrieving media items that are can be played without an 957 * internet connection. 958 * If the media browser service can provide such media items, the implementation must return 959 * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back. 960 * 961 * <p>The root hint may contain multiple keys. 962 * 963 * @see #EXTRA_RECENT 964 * @see #EXTRA_SUGGESTED 965 * @hide 966 */ 967 public static final String EXTRA_OFFLINE = "android.service.media.extra.OFFLINE"; 968 969 /** 970 * The lookup key for a boolean that indicates whether the browser service should return a 971 * browser root for suggested media items. 972 * 973 * <p>When creating a media browser for a given media browser service, this key can be 974 * supplied as a root hint for retrieving the media items suggested by the media browser 975 * service. 976 * If the media browser service can provide such media items, the implementation must return 977 * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back. 978 * 979 * <p>The root hint may contain multiple keys. 980 * 981 * @see #EXTRA_RECENT 982 * @see #EXTRA_OFFLINE 983 * @hide 984 */ 985 public static final String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED"; 986 987 final private String mRootId; 988 final private Bundle mExtras; 989 990 /** 991 * Constructs a browser root. 992 * @param rootId The root id for browsing. 993 * @param extras Any extras about the browser service. 994 */ 995 public BrowserRoot(@NonNull String rootId, @Nullable Bundle extras) { 996 if (rootId == null) { 997 throw new IllegalArgumentException("The root id in BrowserRoot cannot be null. " + 998 "Use null for BrowserRoot instead."); 999 } 1000 mRootId = rootId; 1001 mExtras = extras; 1002 } 1003 1004 /** 1005 * Gets the root id for browsing. 1006 */ 1007 public String getRootId() { 1008 return mRootId; 1009 } 1010 1011 /** 1012 * Gets any extras about the browser service. 1013 */ 1014 public Bundle getExtras() { 1015 return mExtras; 1016 } 1017 } 1018} 1019