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