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