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