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