MediaBrowserServiceCompat.java revision 9703a1e215168b6b580430ec490ca616b6490c80
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.Parcel; 28import android.os.RemoteException; 29import android.support.annotation.NonNull; 30import android.support.annotation.Nullable; 31import android.support.v4.media.session.MediaSessionCompat; 32import android.support.v4.os.ResultReceiver; 33import android.support.v4.util.ArrayMap; 34import android.text.TextUtils; 35import android.util.Log; 36 37import java.io.FileDescriptor; 38import java.io.PrintWriter; 39import java.util.ArrayList; 40import java.util.HashSet; 41import java.util.List; 42 43/** 44 * Base class for media browse services. 45 * <p> 46 * Media browse services enable applications to browse media content provided by an application 47 * and ask the application to start playing it. They may also be used to control content that 48 * is already playing by way of a {@link MediaSessionCompat}. 49 * </p> 50 * 51 * To extend this class, you must declare the service in your manifest file with 52 * an intent filter with the {@link #SERVICE_INTERFACE} action. 53 * 54 * For example: 55 * </p><pre> 56 * <service android:name=".MyMediaBrowserServiceCompat" 57 * android:label="@string/service_name" > 58 * <intent-filter> 59 * <action android:name="android.media.browse.MediaBrowserService" /> 60 * </intent-filter> 61 * </service> 62 * </pre> 63 * @hide 64 */ 65public abstract class MediaBrowserServiceCompat extends Service { 66 private static final String TAG = "MediaBrowserServiceCompat"; 67 private static final boolean DBG = false; 68 69 private MediaBrowserServiceImpl mImpl; 70 71 /** 72 * The {@link Intent} that must be declared as handled by the service. 73 */ 74 public static final String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService"; 75 76 /** 77 * A key for passing the MediaItem to the ResultReceiver in getItem. 78 * 79 * @hide 80 */ 81 public static final String KEY_MEDIA_ITEM = "media_item"; 82 83 private final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap(); 84 private final Handler mHandler = new Handler(); 85 MediaSessionCompat.Token mSession; 86 87 interface MediaBrowserServiceImpl { 88 public void onCreate(); 89 IBinder onBind(Intent intent); 90 } 91 92 class MediaBrowserServiceImplBase implements MediaBrowserServiceImpl { 93 private ServiceBinderCompat mBinder; 94 95 @Override 96 public void onCreate() { 97 mBinder = new ServiceBinderCompat(new ServiceStub()); 98 } 99 100 @Override 101 public IBinder onBind(Intent intent) { 102 // STOPSHIP: Use messenger or version management for further extension 103 if (SERVICE_INTERFACE.equals(intent.getAction())) { 104 return mBinder; 105 } 106 return null; 107 } 108 } 109 110 class MediaBrowserServiceImplApi21 implements MediaBrowserServiceImpl { 111 private Object mServiceObj; 112 113 @Override 114 public void onCreate() { 115 mServiceObj = MediaBrowserServiceCompatApi21.createService(); 116 MediaBrowserServiceCompatApi21.onCreate(mServiceObj, 117 new ServiceStubApi21(new ServiceStub())); 118 } 119 120 @Override 121 public IBinder onBind(Intent intent) { 122 return MediaBrowserServiceCompatApi21.onBind(mServiceObj, intent); 123 } 124 } 125 126 /** 127 * All the info about a connection. 128 */ 129 private class ConnectionRecord { 130 String pkg; 131 Bundle rootHints; 132 ServiceCallbacks callbacks; 133 BrowserRoot root; 134 HashSet<String> subscriptions = new HashSet(); 135 } 136 137 /** 138 * Completion handler for asynchronous callback methods in {@link MediaBrowserServiceCompat}. 139 * <p> 140 * Each of the methods that takes one of these to send the result must call 141 * {@link #sendResult} to respond to the caller with the given results. If those 142 * functions return without calling {@link #sendResult}, they must instead call 143 * {@link #detach} before returning, and then may call {@link #sendResult} when 144 * they are done. If more than one of those methods is called, an exception will 145 * be thrown. 146 * 147 * @see MediaBrowserServiceCompat#onLoadChildren 148 * @see MediaBrowserServiceCompat#onLoadItem 149 */ 150 public static class Result<T> { 151 private Object mDebug; 152 private boolean mDetachCalled; 153 private boolean mSendResultCalled; 154 155 Result(Object debug) { 156 mDebug = debug; 157 } 158 159 /** 160 * Send the result back to the caller. 161 */ 162 public void sendResult(T result) { 163 if (mSendResultCalled) { 164 throw new IllegalStateException("sendResult() called twice for: " + mDebug); 165 } 166 mSendResultCalled = true; 167 onResultSent(result); 168 } 169 170 /** 171 * Detach this message from the current thread and allow the {@link #sendResult} 172 * call to happen later. 173 */ 174 public void detach() { 175 if (mDetachCalled) { 176 throw new IllegalStateException("detach() called when detach() had already" 177 + " been called for: " + mDebug); 178 } 179 if (mSendResultCalled) { 180 throw new IllegalStateException("detach() called when sendResult() had already" 181 + " been called for: " + mDebug); 182 } 183 mDetachCalled = true; 184 } 185 186 boolean isDone() { 187 return mDetachCalled || mSendResultCalled; 188 } 189 190 /** 191 * Called when the result is sent, after assertions about not being called twice 192 * have happened. 193 */ 194 void onResultSent(T result) { 195 } 196 } 197 198 private class ServiceStub { 199 public void connect(final String pkg, final Bundle rootHints, 200 final ServiceCallbacks callbacks) { 201 202 final int uid = Binder.getCallingUid(); 203 if (!isValidPackage(pkg, uid)) { 204 throw new IllegalArgumentException("Package/uid mismatch: uid=" + uid 205 + " package=" + pkg); 206 } 207 208 mHandler.post(new Runnable() { 209 @Override 210 public void run() { 211 final IBinder b = callbacks.asBinder(); 212 213 // Clear out the old subscriptions. We are getting new ones. 214 mConnections.remove(b); 215 216 final ConnectionRecord connection = new ConnectionRecord(); 217 connection.pkg = pkg; 218 connection.rootHints = rootHints; 219 connection.callbacks = callbacks; 220 221 connection.root = 222 MediaBrowserServiceCompat.this.onGetRoot(pkg, uid, rootHints); 223 224 // If they didn't return something, don't allow this client. 225 if (connection.root == null) { 226 Log.i(TAG, "No root for client " + pkg + " from service " 227 + getClass().getName()); 228 try { 229 callbacks.onConnectFailed(); 230 } catch (RemoteException ex) { 231 Log.w(TAG, "Calling onConnectFailed() failed. Ignoring. " 232 + "pkg=" + pkg); 233 } 234 } else { 235 try { 236 mConnections.put(b, connection); 237 if (mSession != null) { 238 callbacks.onConnect(connection.root.getRootId(), 239 mSession, connection.root.getExtras()); 240 } 241 } catch (RemoteException ex) { 242 Log.w(TAG, "Calling onConnect() failed. Dropping client. " 243 + "pkg=" + pkg); 244 mConnections.remove(b); 245 } 246 } 247 } 248 }); 249 } 250 251 public void disconnect(final ServiceCallbacks callbacks) { 252 mHandler.post(new Runnable() { 253 @Override 254 public void run() { 255 final IBinder b = callbacks.asBinder(); 256 257 // Clear out the old subscriptions. We are getting new ones. 258 final ConnectionRecord old = mConnections.remove(b); 259 if (old != null) { 260 // TODO 261 } 262 } 263 }); 264 } 265 266 267 public void addSubscription(final String id, final ServiceCallbacks callbacks) { 268 mHandler.post(new Runnable() { 269 @Override 270 public void run() { 271 final IBinder b = callbacks.asBinder(); 272 273 // Get the record for the connection 274 final ConnectionRecord connection = mConnections.get(b); 275 if (connection == null) { 276 Log.w(TAG, "addSubscription for callback that isn't registered id=" 277 + id); 278 return; 279 } 280 281 MediaBrowserServiceCompat.this.addSubscription(id, connection); 282 } 283 }); 284 } 285 286 public void removeSubscription(final String id, final ServiceCallbacks callbacks) { 287 mHandler.post(new Runnable() { 288 @Override 289 public void run() { 290 final IBinder b = callbacks.asBinder(); 291 292 ConnectionRecord connection = mConnections.get(b); 293 if (connection == null) { 294 Log.w(TAG, "removeSubscription for callback that isn't registered id=" 295 + id); 296 return; 297 } 298 if (!connection.subscriptions.remove(id)) { 299 Log.w(TAG, "removeSubscription called for " + id 300 + " which is not subscribed"); 301 } 302 } 303 }); 304 } 305 306 public void getMediaItem(final String mediaId, final ResultReceiver receiver) { 307 if (TextUtils.isEmpty(mediaId) || receiver == null) { 308 return; 309 } 310 311 mHandler.post(new Runnable() { 312 @Override 313 public void run() { 314 performLoadItem(mediaId, receiver); 315 } 316 }); 317 } 318 } 319 320 private class ServiceBinderCompat extends IMediaBrowserServiceCompat.Stub { 321 final ServiceStub mServiceStub; 322 323 ServiceBinderCompat(ServiceStub binder) { 324 mServiceStub = binder; 325 } 326 327 @Override 328 public void connect(final String pkg, final Bundle rootHints, 329 final IMediaBrowserServiceCompatCallbacks callbacks) { 330 mServiceStub.connect(pkg, rootHints, new ServiceCallbacksCompat(callbacks)); 331 } 332 333 @Override 334 public void disconnect(final IMediaBrowserServiceCompatCallbacks callbacks) { 335 mConnections.get(callbacks.asBinder()); 336 mServiceStub.disconnect(new ServiceCallbacksCompat(callbacks)); 337 } 338 339 340 @Override 341 public void addSubscription( 342 final String id, final IMediaBrowserServiceCompatCallbacks callbacks) { 343 mServiceStub.addSubscription(id, new ServiceCallbacksCompat(callbacks)); 344 } 345 346 @Override 347 public void removeSubscription(final String id, 348 final IMediaBrowserServiceCompatCallbacks callbacks) { 349 mServiceStub.removeSubscription(id, new ServiceCallbacksCompat(callbacks)); 350 } 351 352 @Override 353 public void getMediaItem(final String mediaId, final ResultReceiver receiver) { 354 mServiceStub.getMediaItem(mediaId, receiver); 355 } 356 } 357 358 private class ServiceStubApi21 implements MediaBrowserServiceCompatApi21.ServiceStub { 359 final ServiceStub mBinder; 360 361 ServiceStubApi21(ServiceStub binder) { 362 mBinder = binder; 363 } 364 365 @Override 366 public void connect(final String pkg, final Bundle rootHints, 367 final MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) { 368 mBinder.connect(pkg, rootHints, new ServiceCallbacksApi21(callbacks)); 369 } 370 371 @Override 372 public void disconnect(final MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) { 373 mBinder.disconnect(new ServiceCallbacksApi21(callbacks)); 374 } 375 376 377 @Override 378 public void addSubscription( 379 final String id, final MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) { 380 mBinder.addSubscription(id, new ServiceCallbacksApi21(callbacks)); 381 } 382 383 @Override 384 public void removeSubscription(final String id, 385 final MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) { 386 mBinder.removeSubscription(id, new ServiceCallbacksApi21(callbacks)); 387 } 388 389 @Override 390 public void getMediaItem(final String mediaId, final android.os.ResultReceiver receiver) { 391 ResultReceiver receiverCompat = new ResultReceiver(mHandler) { 392 @Override 393 protected void onReceiveResult(int resultCode, Bundle resultData) { 394 receiver.send(resultCode, resultData); 395 } 396 }; 397 mBinder.getMediaItem(mediaId, receiverCompat); 398 } 399 } 400 401 private interface ServiceCallbacks { 402 IBinder asBinder(); 403 void onConnect(String root, MediaSessionCompat.Token session, Bundle extras) 404 throws RemoteException; 405 void onConnectFailed() throws RemoteException; 406 void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list) 407 throws RemoteException; 408 } 409 410 private class ServiceCallbacksCompat implements ServiceCallbacks { 411 final IMediaBrowserServiceCompatCallbacks mCallbacks; 412 413 ServiceCallbacksCompat(IMediaBrowserServiceCompatCallbacks callbacks) { 414 mCallbacks = callbacks; 415 } 416 417 public IBinder asBinder() { 418 return mCallbacks.asBinder(); 419 } 420 421 public void onConnect(String root, MediaSessionCompat.Token session, Bundle extras) 422 throws RemoteException { 423 mCallbacks.onConnect(root, session, extras); 424 } 425 426 public void onConnectFailed() throws RemoteException { 427 mCallbacks.onConnectFailed(); 428 } 429 430 public void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list) 431 throws RemoteException { 432 mCallbacks.onLoadChildren(mediaId, list); 433 } 434 } 435 436 private class ServiceCallbacksApi21 implements ServiceCallbacks { 437 final MediaBrowserServiceCompatApi21.ServiceCallbacks mCallbacks; 438 439 ServiceCallbacksApi21(MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) { 440 mCallbacks = callbacks; 441 } 442 443 public IBinder asBinder() { 444 return mCallbacks.asBinder(); 445 } 446 447 public void onConnect(String root, MediaSessionCompat.Token session, Bundle extras) 448 throws RemoteException { 449 mCallbacks.onConnect(root, session.getToken(), extras); 450 } 451 452 public void onConnectFailed() throws RemoteException { 453 mCallbacks.onConnectFailed(); 454 } 455 456 public void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list) 457 throws RemoteException { 458 List<Parcel> parcelList = null; 459 if (list != null) { 460 parcelList = new ArrayList<>(); 461 for (MediaBrowserCompat.MediaItem item : list) { 462 Parcel parcel = Parcel.obtain(); 463 item.writeToParcel(parcel, 0); 464 parcelList.add(parcel); 465 } 466 } 467 mCallbacks.onLoadChildren(mediaId, parcelList); 468 } 469 } 470 471 @Override 472 public void onCreate() { 473 super.onCreate(); 474 if (Build.VERSION.SDK_INT >= 21) { 475 mImpl = new MediaBrowserServiceImplApi21(); 476 } else { 477 mImpl = new MediaBrowserServiceImplBase(); 478 } 479 mImpl.onCreate(); 480 } 481 482 @Override 483 public IBinder onBind(Intent intent) { 484 return mImpl.onBind(intent); 485 } 486 487 @Override 488 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 489 } 490 491 /** 492 * Called to get the root information for browsing by a particular client. 493 * <p> 494 * The implementation should verify that the client package has permission 495 * to access browse media information before returning the root id; it 496 * should return null if the client is not allowed to access this 497 * information. 498 * </p> 499 * 500 * @param clientPackageName The package name of the application which is 501 * requesting access to browse media. 502 * @param clientUid The uid of the application which is requesting access to 503 * browse media. 504 * @param rootHints An optional bundle of service-specific arguments to send 505 * to the media browse service when connecting and retrieving the 506 * root id for browsing, or null if none. The contents of this 507 * bundle may affect the information returned when browsing. 508 * @return The {@link BrowserRoot} for accessing this app's content or null. 509 */ 510 public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName, 511 int clientUid, @Nullable Bundle rootHints); 512 513 /** 514 * Called to get information about the children of a media item. 515 * <p> 516 * Implementations must call {@link Result#sendResult result.sendResult} 517 * with the list of children. If loading the children will be an expensive 518 * operation that should be performed on another thread, 519 * {@link Result#detach result.detach} may be called before returning from 520 * this function, and then {@link Result#sendResult result.sendResult} 521 * called when the loading is complete. 522 * 523 * @param parentId The id of the parent media item whose children are to be 524 * queried. 525 * @param result The Result to send the list of children to, or null if the 526 * id is invalid. 527 */ 528 public abstract void onLoadChildren(@NonNull String parentId, 529 @NonNull Result<List<MediaBrowserCompat.MediaItem>> result); 530 531 /** 532 * Called to get information about a specific media item. 533 * <p> 534 * Implementations must call {@link Result#sendResult result.sendResult}. If 535 * loading the item will be an expensive operation {@link Result#detach 536 * result.detach} may be called before returning from this function, and 537 * then {@link Result#sendResult result.sendResult} called when the item has 538 * been loaded. 539 * <p> 540 * The default implementation sends a null result. 541 * 542 * @param itemId The id for the specific 543 * {@link MediaBrowserCompat.MediaItem}. 544 * @param result The Result to send the item to, or null if the id is 545 * invalid. 546 */ 547 public void onLoadItem(String itemId, Result<MediaBrowserCompat.MediaItem> result) { 548 result.sendResult(null); 549 } 550 551 /** 552 * Call to set the media session. 553 * <p> 554 * This should be called as soon as possible during the service's startup. 555 * It may only be called once. 556 * 557 * @param token The token for the service's {@link MediaSessionCompat}. 558 */ 559 public void setSessionToken(final MediaSessionCompat.Token token) { 560 if (token == null) { 561 throw new IllegalArgumentException("Session token may not be null."); 562 } 563 if (mSession != null) { 564 throw new IllegalStateException("The session token has already been set."); 565 } 566 mSession = token; 567 mHandler.post(new Runnable() { 568 @Override 569 public void run() { 570 for (IBinder key : mConnections.keySet()) { 571 ConnectionRecord connection = mConnections.get(key); 572 try { 573 connection.callbacks.onConnect(connection.root.getRootId(), token, 574 connection.root.getExtras()); 575 } catch (RemoteException e) { 576 Log.w(TAG, "Connection for " + connection.pkg + " is no longer valid."); 577 mConnections.remove(key); 578 } 579 } 580 } 581 }); 582 } 583 584 /** 585 * Gets the session token, or null if it has not yet been created 586 * or if it has been destroyed. 587 */ 588 public @Nullable MediaSessionCompat.Token getSessionToken() { 589 return mSession; 590 } 591 592 /** 593 * Notifies all connected media browsers that the children of 594 * the specified parent id have changed in some way. 595 * This will cause browsers to fetch subscribed content again. 596 * 597 * @param parentId The id of the parent media item whose 598 * children changed. 599 */ 600 public void notifyChildrenChanged(@NonNull final String parentId) { 601 if (parentId == null) { 602 throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged"); 603 } 604 mHandler.post(new Runnable() { 605 @Override 606 public void run() { 607 for (IBinder binder : mConnections.keySet()) { 608 ConnectionRecord connection = mConnections.get(binder); 609 if (connection.subscriptions.contains(parentId)) { 610 performLoadChildren(parentId, connection); 611 } 612 } 613 } 614 }); 615 } 616 617 /** 618 * Return whether the given package is one of the ones that is owned by the uid. 619 */ 620 private boolean isValidPackage(String pkg, int uid) { 621 if (pkg == null) { 622 return false; 623 } 624 final PackageManager pm = getPackageManager(); 625 final String[] packages = pm.getPackagesForUid(uid); 626 final int N = packages.length; 627 for (int i=0; i<N; i++) { 628 if (packages[i].equals(pkg)) { 629 return true; 630 } 631 } 632 return false; 633 } 634 635 /** 636 * Save the subscription and if it is a new subscription send the results. 637 */ 638 private void addSubscription(String id, ConnectionRecord connection) { 639 // Save the subscription 640 connection.subscriptions.add(id); 641 642 // send the results 643 performLoadChildren(id, connection); 644 } 645 646 /** 647 * Call onLoadChildren and then send the results back to the connection. 648 * <p> 649 * Callers must make sure that this connection is still connected. 650 */ 651 private void performLoadChildren(final String parentId, final ConnectionRecord connection) { 652 final Result<List<MediaBrowserCompat.MediaItem>> result 653 = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) { 654 @Override 655 void onResultSent(List<MediaBrowserCompat.MediaItem> list) { 656 if (list == null) { 657 throw new IllegalStateException("onLoadChildren sent null list for id " 658 + parentId); 659 } 660 if (mConnections.get(connection.callbacks.asBinder()) != connection) { 661 if (DBG) { 662 Log.d(TAG, "Not sending onLoadChildren result for connection that has" 663 + " been disconnected. pkg=" + connection.pkg + " id=" + parentId); 664 } 665 return; 666 } 667 668 try { 669 connection.callbacks.onLoadChildren(parentId, list); 670 } catch (RemoteException ex) { 671 // The other side is in the process of crashing. 672 Log.w(TAG, "Calling onLoadChildren() failed for id=" + parentId 673 + " package=" + connection.pkg); 674 } 675 } 676 }; 677 678 onLoadChildren(parentId, result); 679 680 if (!result.isDone()) { 681 throw new IllegalStateException("onLoadChildren must call detach() or sendResult()" 682 + " before returning for package=" + connection.pkg + " id=" + parentId); 683 } 684 } 685 686 private void performLoadItem(String itemId, final ResultReceiver receiver) { 687 final Result<MediaBrowserCompat.MediaItem> result = 688 new Result<MediaBrowserCompat.MediaItem>(itemId) { 689 @Override 690 void onResultSent(MediaBrowserCompat.MediaItem item) { 691 Bundle bundle = new Bundle(); 692 bundle.putParcelable(KEY_MEDIA_ITEM, item); 693 receiver.send(0, bundle); 694 } 695 }; 696 697 MediaBrowserServiceCompat.this.onLoadItem(itemId, result); 698 699 if (!result.isDone()) { 700 throw new IllegalStateException("onLoadItem must call detach() or sendResult()" 701 + " before returning for id=" + itemId); 702 } 703 } 704 705 /** 706 * Contains information that the browser service needs to send to the client 707 * when first connected. 708 */ 709 public static final class BrowserRoot { 710 final private String mRootId; 711 final private Bundle mExtras; 712 713 /** 714 * Constructs a browser root. 715 * @param rootId The root id for browsing. 716 * @param extras Any extras about the browser service. 717 */ 718 public BrowserRoot(@NonNull String rootId, @Nullable Bundle extras) { 719 if (rootId == null) { 720 throw new IllegalArgumentException("The root id in BrowserRoot cannot be null. " + 721 "Use null for BrowserRoot instead."); 722 } 723 mRootId = rootId; 724 mExtras = extras; 725 } 726 727 /** 728 * Gets the root id for browsing. 729 */ 730 public String getRootId() { 731 return mRootId; 732 } 733 734 /** 735 * Gets any extras about the brwoser service. 736 */ 737 public Bundle getExtras() { 738 return mExtras; 739 } 740 } 741} 742