MediaBrowserServiceCompat.java revision b768ed3824de2c109411654b3830feabe564ff0a
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 static final String TAG = "MBServiceCompat"; 100 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 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 final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap<>(); 124 ConnectionRecord mCurConnection; 125 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 Parcel parcelItem = Parcel.obtain(); 335 item.writeToParcel(parcelItem, 0); 336 resultWrapper.sendResult(parcelItem); 337 } 338 339 @Override 340 public void detach() { 341 resultWrapper.detach(); 342 } 343 }; 344 MediaBrowserServiceCompat.this.onLoadItem(itemId, result); 345 } 346 } 347 348 class MediaBrowserServiceImplApi24 extends MediaBrowserServiceImplApi23 implements 349 MediaBrowserServiceCompatApi24.ServiceCompatProxy { 350 @Override 351 public void onCreate() { 352 mServiceObj = MediaBrowserServiceCompatApi24.createService( 353 MediaBrowserServiceCompat.this, this); 354 MediaBrowserServiceCompatApi21.onCreate(mServiceObj); 355 } 356 357 @Override 358 public void notifyChildrenChanged(final String parentId, final Bundle options) { 359 if (options == null) { 360 MediaBrowserServiceCompatApi21.notifyChildrenChanged(mServiceObj, parentId); 361 } else { 362 MediaBrowserServiceCompatApi24.notifyChildrenChanged(mServiceObj, parentId, 363 options); 364 } 365 } 366 367 @Override 368 public void onLoadChildren(String parentId, 369 final MediaBrowserServiceCompatApi24.ResultWrapper resultWrapper, Bundle options) { 370 final Result<List<MediaBrowserCompat.MediaItem>> result 371 = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) { 372 @Override 373 void onResultSent(List<MediaBrowserCompat.MediaItem> list, @ResultFlags int flags) { 374 List<Parcel> parcelList = null; 375 if (list != null) { 376 parcelList = new ArrayList<>(); 377 for (MediaBrowserCompat.MediaItem item : list) { 378 Parcel parcel = Parcel.obtain(); 379 item.writeToParcel(parcel, 0); 380 parcelList.add(parcel); 381 } 382 } 383 resultWrapper.sendResult(parcelList, flags); 384 } 385 386 @Override 387 public void detach() { 388 resultWrapper.detach(); 389 } 390 }; 391 MediaBrowserServiceCompat.this.onLoadChildren(parentId, result, options); 392 } 393 394 @Override 395 public Bundle getBrowserRootHints() { 396 return MediaBrowserServiceCompatApi24.getBrowserRootHints(mServiceObj); 397 } 398 } 399 400 private final class ServiceHandler extends Handler { 401 private final ServiceBinderImpl mServiceBinderImpl = new ServiceBinderImpl(); 402 403 ServiceHandler() { 404 } 405 406 @Override 407 public void handleMessage(Message msg) { 408 Bundle data = msg.getData(); 409 switch (msg.what) { 410 case CLIENT_MSG_CONNECT: 411 mServiceBinderImpl.connect(data.getString(DATA_PACKAGE_NAME), 412 data.getInt(DATA_CALLING_UID), data.getBundle(DATA_ROOT_HINTS), 413 new ServiceCallbacksCompat(msg.replyTo)); 414 break; 415 case CLIENT_MSG_DISCONNECT: 416 mServiceBinderImpl.disconnect(new ServiceCallbacksCompat(msg.replyTo)); 417 break; 418 case CLIENT_MSG_ADD_SUBSCRIPTION: 419 mServiceBinderImpl.addSubscription(data.getString(DATA_MEDIA_ITEM_ID), 420 BundleCompat.getBinder(data, DATA_CALLBACK_TOKEN), 421 data.getBundle(DATA_OPTIONS), 422 new ServiceCallbacksCompat(msg.replyTo)); 423 break; 424 case CLIENT_MSG_REMOVE_SUBSCRIPTION: 425 mServiceBinderImpl.removeSubscription(data.getString(DATA_MEDIA_ITEM_ID), 426 BundleCompat.getBinder(data, DATA_CALLBACK_TOKEN), 427 new ServiceCallbacksCompat(msg.replyTo)); 428 break; 429 case CLIENT_MSG_GET_MEDIA_ITEM: 430 mServiceBinderImpl.getMediaItem(data.getString(DATA_MEDIA_ITEM_ID), 431 (ResultReceiver) data.getParcelable(DATA_RESULT_RECEIVER), 432 new ServiceCallbacksCompat(msg.replyTo)); 433 break; 434 case CLIENT_MSG_REGISTER_CALLBACK_MESSENGER: 435 mServiceBinderImpl.registerCallbacks(new ServiceCallbacksCompat(msg.replyTo), 436 data.getBundle(DATA_ROOT_HINTS)); 437 break; 438 case CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER: 439 mServiceBinderImpl.unregisterCallbacks(new ServiceCallbacksCompat(msg.replyTo)); 440 break; 441 default: 442 Log.w(TAG, "Unhandled message: " + msg 443 + "\n Service version: " + SERVICE_VERSION_CURRENT 444 + "\n Client version: " + msg.arg1); 445 } 446 } 447 448 @Override 449 public boolean sendMessageAtTime(Message msg, long uptimeMillis) { 450 // Binder.getCallingUid() in handleMessage will return the uid of this process. 451 // In order to get the right calling uid, Binder.getCallingUid() should be called here. 452 Bundle data = msg.getData(); 453 data.setClassLoader(MediaBrowserCompat.class.getClassLoader()); 454 data.putInt(DATA_CALLING_UID, Binder.getCallingUid()); 455 return super.sendMessageAtTime(msg, uptimeMillis); 456 } 457 458 public void postOrRun(Runnable r) { 459 if (Thread.currentThread() == getLooper().getThread()) { 460 r.run(); 461 } else { 462 post(r); 463 } 464 } 465 } 466 467 /** 468 * All the info about a connection. 469 */ 470 private class ConnectionRecord { 471 String pkg; 472 Bundle rootHints; 473 ServiceCallbacks callbacks; 474 BrowserRoot root; 475 HashMap<String, List<Pair<IBinder, Bundle>>> subscriptions = new HashMap(); 476 477 ConnectionRecord() { 478 } 479 } 480 481 /** 482 * Completion handler for asynchronous callback methods in {@link MediaBrowserServiceCompat}. 483 * <p> 484 * Each of the methods that takes one of these to send the result must call 485 * {@link #sendResult} to respond to the caller with the given results. If those 486 * functions return without calling {@link #sendResult}, they must instead call 487 * {@link #detach} before returning, and then may call {@link #sendResult} when 488 * they are done. If more than one of those methods is called, an exception will 489 * be thrown. 490 * 491 * @see MediaBrowserServiceCompat#onLoadChildren 492 * @see MediaBrowserServiceCompat#onLoadItem 493 */ 494 public static class Result<T> { 495 private Object mDebug; 496 private boolean mDetachCalled; 497 private boolean mSendResultCalled; 498 private int mFlags; 499 500 Result(Object debug) { 501 mDebug = debug; 502 } 503 504 /** 505 * Send the result back to the caller. 506 */ 507 public void sendResult(T result) { 508 if (mSendResultCalled) { 509 throw new IllegalStateException("sendResult() called twice for: " + mDebug); 510 } 511 mSendResultCalled = true; 512 onResultSent(result, mFlags); 513 } 514 515 /** 516 * Detach this message from the current thread and allow the {@link #sendResult} 517 * call to happen later. 518 */ 519 public void detach() { 520 if (mDetachCalled) { 521 throw new IllegalStateException("detach() called when detach() had already" 522 + " been called for: " + mDebug); 523 } 524 if (mSendResultCalled) { 525 throw new IllegalStateException("detach() called when sendResult() had already" 526 + " been called for: " + mDebug); 527 } 528 mDetachCalled = true; 529 } 530 531 boolean isDone() { 532 return mDetachCalled || mSendResultCalled; 533 } 534 535 void setFlags(@ResultFlags int flags) { 536 mFlags = flags; 537 } 538 539 /** 540 * Called when the result is sent, after assertions about not being called twice 541 * have happened. 542 */ 543 void onResultSent(T result, @ResultFlags int flags) { 544 } 545 } 546 547 private class ServiceBinderImpl { 548 ServiceBinderImpl() { 549 } 550 551 public void connect(final String pkg, final int uid, final Bundle rootHints, 552 final ServiceCallbacks callbacks) { 553 554 if (!isValidPackage(pkg, uid)) { 555 throw new IllegalArgumentException("Package/uid mismatch: uid=" + uid 556 + " package=" + pkg); 557 } 558 559 mHandler.postOrRun(new Runnable() { 560 @Override 561 public void run() { 562 final IBinder b = callbacks.asBinder(); 563 564 // Clear out the old subscriptions. We are getting new ones. 565 mConnections.remove(b); 566 567 final ConnectionRecord connection = new ConnectionRecord(); 568 connection.pkg = pkg; 569 connection.rootHints = rootHints; 570 connection.callbacks = callbacks; 571 572 connection.root = 573 MediaBrowserServiceCompat.this.onGetRoot(pkg, uid, rootHints); 574 575 // If they didn't return something, don't allow this client. 576 if (connection.root == null) { 577 Log.i(TAG, "No root for client " + pkg + " from service " 578 + getClass().getName()); 579 try { 580 callbacks.onConnectFailed(); 581 } catch (RemoteException ex) { 582 Log.w(TAG, "Calling onConnectFailed() failed. Ignoring. " 583 + "pkg=" + pkg); 584 } 585 } else { 586 try { 587 mConnections.put(b, connection); 588 if (mSession != null) { 589 callbacks.onConnect(connection.root.getRootId(), 590 mSession, connection.root.getExtras()); 591 } 592 } catch (RemoteException ex) { 593 Log.w(TAG, "Calling onConnect() failed. Dropping client. " 594 + "pkg=" + pkg); 595 mConnections.remove(b); 596 } 597 } 598 } 599 }); 600 } 601 602 public void disconnect(final ServiceCallbacks callbacks) { 603 mHandler.postOrRun(new Runnable() { 604 @Override 605 public void run() { 606 final IBinder b = callbacks.asBinder(); 607 608 // Clear out the old subscriptions. We are getting new ones. 609 final ConnectionRecord old = mConnections.remove(b); 610 if (old != null) { 611 // TODO 612 } 613 } 614 }); 615 } 616 617 public void addSubscription(final String id, final IBinder token, final Bundle options, 618 final ServiceCallbacks callbacks) { 619 mHandler.postOrRun(new Runnable() { 620 @Override 621 public void run() { 622 final IBinder b = callbacks.asBinder(); 623 624 // Get the record for the connection 625 final ConnectionRecord connection = mConnections.get(b); 626 if (connection == null) { 627 Log.w(TAG, "addSubscription for callback that isn't registered id=" 628 + id); 629 return; 630 } 631 632 MediaBrowserServiceCompat.this.addSubscription(id, connection, token, options); 633 } 634 }); 635 } 636 637 public void removeSubscription(final String id, final IBinder token, 638 final ServiceCallbacks callbacks) { 639 mHandler.postOrRun(new Runnable() { 640 @Override 641 public void run() { 642 final IBinder b = callbacks.asBinder(); 643 644 ConnectionRecord connection = mConnections.get(b); 645 if (connection == null) { 646 Log.w(TAG, "removeSubscription for callback that isn't registered id=" 647 + id); 648 return; 649 } 650 if (!MediaBrowserServiceCompat.this.removeSubscription( 651 id, connection, token)) { 652 Log.w(TAG, "removeSubscription called for " + id 653 + " which is not subscribed"); 654 } 655 } 656 }); 657 } 658 659 public void getMediaItem(final String mediaId, final ResultReceiver receiver, 660 final ServiceCallbacks callbacks) { 661 if (TextUtils.isEmpty(mediaId) || receiver == null) { 662 return; 663 } 664 665 mHandler.postOrRun(new Runnable() { 666 @Override 667 public void run() { 668 final IBinder b = callbacks.asBinder(); 669 670 ConnectionRecord connection = mConnections.get(b); 671 if (connection == null) { 672 Log.w(TAG, "getMediaItem for callback that isn't registered id=" + mediaId); 673 return; 674 } 675 performLoadItem(mediaId, connection, receiver); 676 } 677 }); 678 } 679 680 // Used when {@link MediaBrowserProtocol#EXTRA_MESSENGER_BINDER} is used. 681 public void registerCallbacks(final ServiceCallbacks callbacks, final Bundle rootHints) { 682 mHandler.postOrRun(new Runnable() { 683 @Override 684 public void run() { 685 final IBinder b = callbacks.asBinder(); 686 // Clear out the old subscriptions. We are getting new ones. 687 mConnections.remove(b); 688 689 final ConnectionRecord connection = new ConnectionRecord(); 690 connection.callbacks = callbacks; 691 connection.rootHints = rootHints; 692 mConnections.put(b, connection); 693 } 694 }); 695 } 696 697 // Used when {@link MediaBrowserProtocol#EXTRA_MESSENGER_BINDER} is used. 698 public void unregisterCallbacks(final ServiceCallbacks callbacks) { 699 mHandler.postOrRun(new Runnable() { 700 @Override 701 public void run() { 702 final IBinder b = callbacks.asBinder(); 703 mConnections.remove(b); 704 } 705 }); 706 } 707 } 708 709 private interface ServiceCallbacks { 710 IBinder asBinder(); 711 void onConnect(String root, MediaSessionCompat.Token session, Bundle extras) 712 throws RemoteException; 713 void onConnectFailed() throws RemoteException; 714 void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, Bundle options) 715 throws RemoteException; 716 } 717 718 private class ServiceCallbacksCompat implements ServiceCallbacks { 719 final Messenger mCallbacks; 720 721 ServiceCallbacksCompat(Messenger callbacks) { 722 mCallbacks = callbacks; 723 } 724 725 @Override 726 public IBinder asBinder() { 727 return mCallbacks.getBinder(); 728 } 729 730 @Override 731 public void onConnect(String root, MediaSessionCompat.Token session, Bundle extras) 732 throws RemoteException { 733 if (extras == null) { 734 extras = new Bundle(); 735 } 736 extras.putInt(EXTRA_SERVICE_VERSION, SERVICE_VERSION_CURRENT); 737 Bundle data = new Bundle(); 738 data.putString(DATA_MEDIA_ITEM_ID, root); 739 data.putParcelable(DATA_MEDIA_SESSION_TOKEN, session); 740 data.putBundle(DATA_ROOT_HINTS, extras); 741 sendRequest(SERVICE_MSG_ON_CONNECT, data); 742 } 743 744 @Override 745 public void onConnectFailed() throws RemoteException { 746 sendRequest(SERVICE_MSG_ON_CONNECT_FAILED, null); 747 } 748 749 @Override 750 public void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, 751 Bundle options) throws RemoteException { 752 Bundle data = new Bundle(); 753 data.putString(DATA_MEDIA_ITEM_ID, mediaId); 754 data.putBundle(DATA_OPTIONS, options); 755 if (list != null) { 756 data.putParcelableArrayList(DATA_MEDIA_ITEM_LIST, 757 list instanceof ArrayList ? (ArrayList) list : new ArrayList<>(list)); 758 } 759 sendRequest(SERVICE_MSG_ON_LOAD_CHILDREN, data); 760 } 761 762 private void sendRequest(int what, Bundle data) throws RemoteException { 763 Message msg = Message.obtain(); 764 msg.what = what; 765 msg.arg1 = SERVICE_VERSION_CURRENT; 766 msg.setData(data); 767 mCallbacks.send(msg); 768 } 769 } 770 771 @Override 772 public void onCreate() { 773 super.onCreate(); 774 if (Build.VERSION.SDK_INT >= 24 || BuildCompat.isAtLeastN()) { 775 mImpl = new MediaBrowserServiceImplApi24(); 776 } else if (Build.VERSION.SDK_INT >= 23) { 777 mImpl = new MediaBrowserServiceImplApi23(); 778 } else if (Build.VERSION.SDK_INT >= 21) { 779 mImpl = new MediaBrowserServiceImplApi21(); 780 } else { 781 mImpl = new MediaBrowserServiceImplBase(); 782 } 783 mImpl.onCreate(); 784 } 785 786 @Override 787 public IBinder onBind(Intent intent) { 788 return mImpl.onBind(intent); 789 } 790 791 @Override 792 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 793 } 794 795 /** 796 * Called to get the root information for browsing by a particular client. 797 * <p> 798 * The implementation should verify that the client package has permission 799 * to access browse media information before returning the root id; it 800 * should return null if the client is not allowed to access this 801 * information. 802 * </p> 803 * 804 * @param clientPackageName The package name of the application which is 805 * requesting access to browse media. 806 * @param clientUid The uid of the application which is requesting access to 807 * browse media. 808 * @param rootHints An optional bundle of service-specific arguments to send 809 * to the media browse service when connecting and retrieving the 810 * root id for browsing, or null if none. The contents of this 811 * bundle may affect the information returned when browsing. 812 * @return The {@link BrowserRoot} for accessing this app's content or null. 813 * @see BrowserRoot#EXTRA_RECENT 814 * @see BrowserRoot#EXTRA_OFFLINE 815 * @see BrowserRoot#EXTRA_SUGGESTED 816 * @see BrowserRoot#EXTRA_SUGGESTION_KEYWORDS 817 */ 818 public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName, 819 int clientUid, @Nullable Bundle rootHints); 820 821 /** 822 * Called to get information about the children of a media item. 823 * <p> 824 * Implementations must call {@link Result#sendResult result.sendResult} 825 * with the list of children. If loading the children will be an expensive 826 * operation that should be performed on another thread, 827 * {@link Result#detach result.detach} may be called before returning from 828 * this function, and then {@link Result#sendResult result.sendResult} 829 * called when the loading is complete. 830 * 831 * @param parentId The id of the parent media item whose children are to be 832 * queried. 833 * @param result The Result to send the list of children to, or null if the 834 * id is invalid. 835 */ 836 public abstract void onLoadChildren(@NonNull String parentId, 837 @NonNull Result<List<MediaBrowserCompat.MediaItem>> result); 838 839 /** 840 * Called to get information about the children of a media item. 841 * <p> 842 * Implementations must call {@link Result#sendResult result.sendResult} 843 * with the list of children. If loading the children will be an expensive 844 * operation that should be performed on another thread, 845 * {@link Result#detach result.detach} may be called before returning from 846 * this function, and then {@link Result#sendResult result.sendResult} 847 * called when the loading is complete. 848 * 849 * @param parentId The id of the parent media item whose children are to be 850 * queried. 851 * @param result The Result to send the list of children to, or null if the 852 * id is invalid. 853 * @param options A bundle of service-specific arguments sent from the media 854 * browse. The information returned through the result should be 855 * affected by the contents of this bundle. 856 */ 857 public void onLoadChildren(@NonNull String parentId, 858 @NonNull Result<List<MediaBrowserCompat.MediaItem>> result, @NonNull Bundle options) { 859 // To support backward compatibility, when the implementation of MediaBrowserService doesn't 860 // override onLoadChildren() with options, onLoadChildren() without options will be used 861 // instead, and the options will be applied in the implementation of result.onResultSent(). 862 result.setFlags(RESULT_FLAG_OPTION_NOT_HANDLED); 863 onLoadChildren(parentId, result); 864 } 865 866 /** 867 * Called to get information about a specific media item. 868 * <p> 869 * Implementations must call {@link Result#sendResult result.sendResult}. If 870 * loading the item will be an expensive operation {@link Result#detach 871 * result.detach} may be called before returning from this function, and 872 * then {@link Result#sendResult result.sendResult} called when the item has 873 * been loaded. 874 * <p> 875 * The default implementation sends a null result. 876 * 877 * @param itemId The id for the specific {@link MediaBrowserCompat.MediaItem}. 878 * @param result The Result to send the item to, or null if the id is 879 * invalid. 880 */ 881 public void onLoadItem(String itemId, Result<MediaBrowserCompat.MediaItem> result) { 882 result.sendResult(null); 883 } 884 885 /** 886 * Call to set the media session. 887 * <p> 888 * This should be called as soon as possible during the service's startup. 889 * It may only be called once. 890 * 891 * @param token The token for the service's {@link MediaSessionCompat}. 892 */ 893 public void setSessionToken(MediaSessionCompat.Token token) { 894 if (token == null) { 895 throw new IllegalArgumentException("Session token may not be null."); 896 } 897 if (mSession != null) { 898 throw new IllegalStateException("The session token has already been set."); 899 } 900 mSession = token; 901 mImpl.setSessionToken(token); 902 } 903 904 /** 905 * Gets the session token, or null if it has not yet been created 906 * or if it has been destroyed. 907 */ 908 public @Nullable MediaSessionCompat.Token getSessionToken() { 909 return mSession; 910 } 911 912 /** 913 * Gets the root hints sent from the currently connected {@link MediaBrowserCompat}. 914 * The root hints are service-specific arguments included in an optional bundle sent to the 915 * media browser service when connecting and retrieving the root id for browsing, or null if 916 * none. The contents of this bundle may affect the information returned when browsing. 917 * <p> 918 * Note that this will return null when connected to {@link android.media.browse.MediaBrowser} 919 * and running on API 23 or lower. 920 * 921 * @throws IllegalStateException If this method is called outside of {@link #onLoadChildren} 922 * or {@link #onLoadItem} 923 * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_RECENT 924 * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_OFFLINE 925 * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_SUGGESTED 926 * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_SUGGESTION_KEYWORDS 927 */ 928 public final Bundle getBrowserRootHints() { 929 return mImpl.getBrowserRootHints(); 930 } 931 932 /** 933 * Notifies all connected media browsers that the children of 934 * the specified parent id have changed in some way. 935 * This will cause browsers to fetch subscribed content again. 936 * 937 * @param parentId The id of the parent media item whose 938 * children changed. 939 */ 940 public void notifyChildrenChanged(@NonNull String parentId) { 941 if (parentId == null) { 942 throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged"); 943 } 944 mImpl.notifyChildrenChanged(parentId, null); 945 } 946 947 /** 948 * Notifies all connected media browsers that the children of 949 * the specified parent id have changed in some way. 950 * This will cause browsers to fetch subscribed content again. 951 * 952 * @param parentId The id of the parent media item whose 953 * children changed. 954 * @param options A bundle of service-specific arguments to send 955 * to the media browse. The contents of this bundle may 956 * contain the information about the change. 957 */ 958 public void notifyChildrenChanged(@NonNull String parentId, @NonNull Bundle options) { 959 if (parentId == null) { 960 throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged"); 961 } 962 if (options == null) { 963 throw new IllegalArgumentException("options cannot be null in notifyChildrenChanged"); 964 } 965 mImpl.notifyChildrenChanged(parentId, options); 966 } 967 968 /** 969 * Return whether the given package is one of the ones that is owned by the uid. 970 */ 971 boolean isValidPackage(String pkg, int uid) { 972 if (pkg == null) { 973 return false; 974 } 975 final PackageManager pm = getPackageManager(); 976 final String[] packages = pm.getPackagesForUid(uid); 977 final int N = packages.length; 978 for (int i=0; i<N; i++) { 979 if (packages[i].equals(pkg)) { 980 return true; 981 } 982 } 983 return false; 984 } 985 986 /** 987 * Save the subscription and if it is a new subscription send the results. 988 */ 989 void addSubscription(String id, ConnectionRecord connection, IBinder token, 990 Bundle options) { 991 // Save the subscription 992 List<Pair<IBinder, Bundle>> callbackList = connection.subscriptions.get(id); 993 if (callbackList == null) { 994 callbackList = new ArrayList<>(); 995 } 996 for (Pair<IBinder, Bundle> callback : callbackList) { 997 if (token == callback.first 998 && MediaBrowserCompatUtils.areSameOptions(options, callback.second)) { 999 return; 1000 } 1001 } 1002 callbackList.add(new Pair<>(token, options)); 1003 connection.subscriptions.put(id, callbackList); 1004 // send the results 1005 performLoadChildren(id, connection, options); 1006 } 1007 1008 /** 1009 * Remove the subscription. 1010 */ 1011 boolean removeSubscription(String id, ConnectionRecord connection, IBinder token) { 1012 if (token == null) { 1013 return connection.subscriptions.remove(id) != null; 1014 } 1015 boolean removed = false; 1016 List<Pair<IBinder, Bundle>> callbackList = connection.subscriptions.get(id); 1017 if (callbackList != null) { 1018 Iterator<Pair<IBinder, Bundle>> iter = callbackList.iterator(); 1019 while (iter.hasNext()){ 1020 if (token == iter.next().first) { 1021 removed = true; 1022 iter.remove(); 1023 } 1024 } 1025 if (callbackList.size() == 0) { 1026 connection.subscriptions.remove(id); 1027 } 1028 } 1029 return removed; 1030 } 1031 1032 /** 1033 * Call onLoadChildren and then send the results back to the connection. 1034 * <p> 1035 * Callers must make sure that this connection is still connected. 1036 */ 1037 void performLoadChildren(final String parentId, final ConnectionRecord connection, 1038 final Bundle options) { 1039 final Result<List<MediaBrowserCompat.MediaItem>> result 1040 = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) { 1041 @Override 1042 void onResultSent(List<MediaBrowserCompat.MediaItem> list, @ResultFlags int flags) { 1043 if (mConnections.get(connection.callbacks.asBinder()) != connection) { 1044 if (DEBUG) { 1045 Log.d(TAG, "Not sending onLoadChildren result for connection that has" 1046 + " been disconnected. pkg=" + connection.pkg + " id=" + parentId); 1047 } 1048 return; 1049 } 1050 1051 List<MediaBrowserCompat.MediaItem> filteredList = 1052 (flags & RESULT_FLAG_OPTION_NOT_HANDLED) != 0 1053 ? applyOptions(list, options) : list; 1054 try { 1055 connection.callbacks.onLoadChildren(parentId, filteredList, options); 1056 } catch (RemoteException ex) { 1057 // The other side is in the process of crashing. 1058 Log.w(TAG, "Calling onLoadChildren() failed for id=" + parentId 1059 + " package=" + connection.pkg); 1060 } 1061 } 1062 }; 1063 1064 mCurConnection = connection; 1065 if (options == null) { 1066 onLoadChildren(parentId, result); 1067 } else { 1068 onLoadChildren(parentId, result, options); 1069 } 1070 mCurConnection = null; 1071 1072 if (!result.isDone()) { 1073 throw new IllegalStateException("onLoadChildren must call detach() or sendResult()" 1074 + " before returning for package=" + connection.pkg + " id=" + parentId); 1075 } 1076 } 1077 1078 List<MediaBrowserCompat.MediaItem> applyOptions(List<MediaBrowserCompat.MediaItem> list, 1079 final Bundle options) { 1080 if (list == null) { 1081 return null; 1082 } 1083 int page = options.getInt(MediaBrowserCompat.EXTRA_PAGE, -1); 1084 int pageSize = options.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, -1); 1085 if (page == -1 && pageSize == -1) { 1086 return list; 1087 } 1088 int fromIndex = pageSize * page; 1089 int toIndex = fromIndex + pageSize; 1090 if (page < 0 || pageSize < 1 || fromIndex >= list.size()) { 1091 return Collections.EMPTY_LIST; 1092 } 1093 if (toIndex > list.size()) { 1094 toIndex = list.size(); 1095 } 1096 return list.subList(fromIndex, toIndex); 1097 } 1098 1099 void performLoadItem(String itemId, ConnectionRecord connection, 1100 final ResultReceiver receiver) { 1101 final Result<MediaBrowserCompat.MediaItem> result = 1102 new Result<MediaBrowserCompat.MediaItem>(itemId) { 1103 @Override 1104 void onResultSent(MediaBrowserCompat.MediaItem item, @ResultFlags int flags) { 1105 Bundle bundle = new Bundle(); 1106 bundle.putParcelable(KEY_MEDIA_ITEM, item); 1107 receiver.send(0, bundle); 1108 } 1109 }; 1110 1111 mCurConnection = connection; 1112 onLoadItem(itemId, result); 1113 mCurConnection = null; 1114 1115 if (!result.isDone()) { 1116 throw new IllegalStateException("onLoadItem must call detach() or sendResult()" 1117 + " before returning for id=" + itemId); 1118 } 1119 } 1120 1121 /** 1122 * Contains information that the browser service needs to send to the client 1123 * when first connected. 1124 */ 1125 public static final class BrowserRoot { 1126 /** 1127 * The lookup key for a boolean that indicates whether the browser service should return a 1128 * browser root for recently played media items. 1129 * 1130 * <p>When creating a media browser for a given media browser service, this key can be 1131 * supplied as a root hint for retrieving media items that are recently played. 1132 * If the media browser service can provide such media items, the implementation must return 1133 * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back. 1134 * 1135 * <p>The root hint may contain multiple keys. 1136 * 1137 * @see #EXTRA_OFFLINE 1138 * @see #EXTRA_SUGGESTED 1139 * @see #EXTRA_SUGGESTION_KEYWORDS 1140 */ 1141 public static final String EXTRA_RECENT = "android.service.media.extra.RECENT"; 1142 1143 /** 1144 * The lookup key for a boolean that indicates whether the browser service should return a 1145 * browser root for offline media items. 1146 * 1147 * <p>When creating a media browser for a given media browser service, this key can be 1148 * supplied as a root hint for retrieving media items that are can be played without an 1149 * internet connection. 1150 * If the media browser service can provide such media items, the implementation must return 1151 * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back. 1152 * 1153 * <p>The root hint may contain multiple keys. 1154 * 1155 * @see #EXTRA_RECENT 1156 * @see #EXTRA_SUGGESTED 1157 * @see #EXTRA_SUGGESTION_KEYWORDS 1158 */ 1159 public static final String EXTRA_OFFLINE = "android.service.media.extra.OFFLINE"; 1160 1161 /** 1162 * The lookup key for a boolean that indicates whether the browser service should return a 1163 * browser root for suggested media items. 1164 * 1165 * <p>When creating a media browser for a given media browser service, this key can be 1166 * supplied as a root hint for retrieving the media items suggested by the media browser 1167 * service. The list of media items passed in {@link android.support.v4.media.MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded(String, List)} 1168 * is considered ordered by relevance, first being the top suggestion. 1169 * If the media browser service can provide such media items, the implementation must return 1170 * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back. 1171 * 1172 * <p>The root hint may contain multiple keys. 1173 * 1174 * @see #EXTRA_RECENT 1175 * @see #EXTRA_OFFLINE 1176 * @see #EXTRA_SUGGESTION_KEYWORDS 1177 */ 1178 public static final String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED"; 1179 1180 /** 1181 * The lookup key for a string that indicates specific keywords which will be considered 1182 * when the browser service suggests media items. 1183 * 1184 * <p>When creating a media browser for a given media browser service, this key can be 1185 * supplied as a root hint together with {@link #EXTRA_SUGGESTED} for retrieving suggested 1186 * media items related with the keywords. The list of media items passed in 1187 * {@link android.media.browse.MediaBrowser.SubscriptionCallback#onChildrenLoaded(String, List)} 1188 * is considered ordered by relevance, first being the top suggestion. 1189 * If the media browser service can provide such media items, the implementation must return 1190 * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back. 1191 * 1192 * <p>The root hint may contain multiple keys. 1193 * 1194 * @see #EXTRA_RECENT 1195 * @see #EXTRA_OFFLINE 1196 * @see #EXTRA_SUGGESTED 1197 */ 1198 public static final String EXTRA_SUGGESTION_KEYWORDS 1199 = "android.service.media.extra.SUGGESTION_KEYWORDS"; 1200 1201 final private String mRootId; 1202 final private Bundle mExtras; 1203 1204 /** 1205 * Constructs a browser root. 1206 * @param rootId The root id for browsing. 1207 * @param extras Any extras about the browser service. 1208 */ 1209 public BrowserRoot(@NonNull String rootId, @Nullable Bundle extras) { 1210 if (rootId == null) { 1211 throw new IllegalArgumentException("The root id in BrowserRoot cannot be null. " + 1212 "Use null for BrowserRoot instead."); 1213 } 1214 mRootId = rootId; 1215 mExtras = extras; 1216 } 1217 1218 /** 1219 * Gets the root id for browsing. 1220 */ 1221 public String getRootId() { 1222 return mRootId; 1223 } 1224 1225 /** 1226 * Gets any extras about the browser service. 1227 */ 1228 public Bundle getExtras() { 1229 return mExtras; 1230 } 1231 } 1232} 1233