1/* 2 * Copyright 2018 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 androidx.media; 18 19import static androidx.annotation.RestrictTo.Scope.LIBRARY; 20import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_ADD_SUBSCRIPTION; 21import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_CONNECT; 22import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_DISCONNECT; 23import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_GET_MEDIA_ITEM; 24import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_REGISTER_CALLBACK_MESSENGER; 25import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_REMOVE_SUBSCRIPTION; 26import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_SEARCH; 27import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_SEND_CUSTOM_ACTION; 28import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER; 29import static androidx.media.MediaBrowserProtocol.DATA_CALLBACK_TOKEN; 30import static androidx.media.MediaBrowserProtocol.DATA_CALLING_PID; 31import static androidx.media.MediaBrowserProtocol.DATA_CALLING_UID; 32import static androidx.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION; 33import static androidx.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION_EXTRAS; 34import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_ID; 35import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_LIST; 36import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_SESSION_TOKEN; 37import static androidx.media.MediaBrowserProtocol.DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS; 38import static androidx.media.MediaBrowserProtocol.DATA_OPTIONS; 39import static androidx.media.MediaBrowserProtocol.DATA_PACKAGE_NAME; 40import static androidx.media.MediaBrowserProtocol.DATA_RESULT_RECEIVER; 41import static androidx.media.MediaBrowserProtocol.DATA_ROOT_HINTS; 42import static androidx.media.MediaBrowserProtocol.DATA_SEARCH_EXTRAS; 43import static androidx.media.MediaBrowserProtocol.DATA_SEARCH_QUERY; 44import static androidx.media.MediaBrowserProtocol.EXTRA_CLIENT_VERSION; 45import static androidx.media.MediaBrowserProtocol.EXTRA_MESSENGER_BINDER; 46import static androidx.media.MediaBrowserProtocol.EXTRA_SERVICE_VERSION; 47import static androidx.media.MediaBrowserProtocol.EXTRA_SESSION_BINDER; 48import static androidx.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT; 49import static androidx.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT_FAILED; 50import static androidx.media.MediaBrowserProtocol.SERVICE_MSG_ON_LOAD_CHILDREN; 51import static androidx.media.MediaBrowserProtocol.SERVICE_VERSION_CURRENT; 52 53import android.app.Service; 54import android.content.Context; 55import android.content.Intent; 56import android.content.pm.PackageManager; 57import android.os.Binder; 58import android.os.Build; 59import android.os.Bundle; 60import android.os.Handler; 61import android.os.IBinder; 62import android.os.Message; 63import android.os.Messenger; 64import android.os.Parcel; 65import android.os.RemoteException; 66import android.service.media.MediaBrowserService; 67import android.support.v4.media.MediaBrowserCompat; 68import android.support.v4.media.session.IMediaSession; 69import android.support.v4.media.session.MediaSessionCompat; 70import android.support.v4.os.ResultReceiver; 71import android.text.TextUtils; 72import android.util.Log; 73 74import androidx.annotation.IntDef; 75import androidx.annotation.NonNull; 76import androidx.annotation.Nullable; 77import androidx.annotation.RequiresApi; 78import androidx.annotation.RestrictTo; 79import androidx.collection.ArrayMap; 80import androidx.core.app.BundleCompat; 81import androidx.core.util.Pair; 82import androidx.media.MediaSessionManager.RemoteUserInfo; 83 84import java.io.FileDescriptor; 85import java.io.PrintWriter; 86import java.lang.annotation.Retention; 87import java.lang.annotation.RetentionPolicy; 88import java.util.ArrayList; 89import java.util.Collections; 90import java.util.HashMap; 91import java.util.Iterator; 92import java.util.List; 93 94/** 95 * Base class for media browse services. 96 * <p> 97 * Media browse services enable applications to browse media content provided by an application 98 * and ask the application to start playing it. They may also be used to control content that 99 * is already playing by way of a {@link MediaSessionCompat}. 100 * </p> 101 * 102 * To extend this class, you must declare the service in your manifest file with 103 * an intent filter with the {@link #SERVICE_INTERFACE} action. 104 * 105 * For example: 106 * </p><pre> 107 * <service android:name=".MyMediaBrowserServiceCompat" 108 * android:label="@string/service_name" > 109 * <intent-filter> 110 * <action android:name="android.media.browse.MediaBrowserService" /> 111 * </intent-filter> 112 * </service> 113 * </pre> 114 * 115 * <div class="special reference"> 116 * <h3>Developer Guides</h3> 117 * <p>For information about building your media application, read the 118 * <a href="{@docRoot}guide/topics/media-apps/index.html">Media Apps</a> developer guide.</p> 119 * </div> 120 */ 121public abstract class MediaBrowserServiceCompat extends Service { 122 static final String TAG = "MBServiceCompat"; 123 static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 124 125 private static final float EPSILON = 0.00001f; 126 127 private MediaBrowserServiceImpl mImpl; 128 129 /** 130 * The {@link Intent} that must be declared as handled by the service. 131 */ 132 public static final String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService"; 133 134 /** 135 * A key for passing the MediaItem to the ResultReceiver in getItem. 136 * 137 * @hide 138 */ 139 @RestrictTo(LIBRARY) 140 public static final String KEY_MEDIA_ITEM = "media_item"; 141 142 /** 143 * A key for passing the list of MediaItems to the ResultReceiver in search. 144 * 145 * @hide 146 */ 147 @RestrictTo(LIBRARY) 148 public static final String KEY_SEARCH_RESULTS = "search_results"; 149 150 static final int RESULT_FLAG_OPTION_NOT_HANDLED = 1 << 0; 151 static final int RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED = 1 << 1; 152 static final int RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED = 1 << 2; 153 154 /** 155 * @hide 156 */ 157 @RestrictTo(LIBRARY) 158 public static final int RESULT_ERROR = -1; 159 /** 160 * @hide 161 */ 162 @RestrictTo(LIBRARY) 163 public static final int RESULT_OK = 0; 164 165 /** 166 * @hide 167 */ 168 @RestrictTo(LIBRARY) 169 public static final int RESULT_PROGRESS_UPDATE = 1; 170 171 /** @hide */ 172 @RestrictTo(LIBRARY) 173 @Retention(RetentionPolicy.SOURCE) 174 @IntDef(flag = true, value = {RESULT_FLAG_OPTION_NOT_HANDLED, 175 RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED, RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED}) 176 private @interface ResultFlags { 177 } 178 179 final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap<>(); 180 ConnectionRecord mCurConnection; 181 final ServiceHandler mHandler = new ServiceHandler(); 182 MediaSessionCompat.Token mSession; 183 184 interface MediaBrowserServiceImpl { 185 void onCreate(); 186 IBinder onBind(Intent intent); 187 void setSessionToken(MediaSessionCompat.Token token); 188 void notifyChildrenChanged(final String parentId, final Bundle options); 189 Bundle getBrowserRootHints(); 190 RemoteUserInfo getCurrentBrowserInfo(); 191 List<RemoteUserInfo> getSubscribingBrowsers(String parentId); 192 } 193 194 class MediaBrowserServiceImplBase implements MediaBrowserServiceImpl { 195 private Messenger mMessenger; 196 197 @Override 198 public void onCreate() { 199 mMessenger = new Messenger(mHandler); 200 } 201 202 @Override 203 public IBinder onBind(Intent intent) { 204 if (SERVICE_INTERFACE.equals(intent.getAction())) { 205 return mMessenger.getBinder(); 206 } 207 return null; 208 } 209 210 @Override 211 public void setSessionToken(final MediaSessionCompat.Token token) { 212 mHandler.post(new Runnable() { 213 @Override 214 public void run() { 215 Iterator<ConnectionRecord> iter = mConnections.values().iterator(); 216 while (iter.hasNext()) { 217 ConnectionRecord connection = iter.next(); 218 try { 219 connection.callbacks.onConnect(connection.root.getRootId(), token, 220 connection.root.getExtras()); 221 } catch (RemoteException e) { 222 Log.w(TAG, "Connection for " + connection.pkg + " is no longer valid."); 223 iter.remove(); 224 } 225 } 226 } 227 }); 228 } 229 230 @Override 231 public void notifyChildrenChanged(@NonNull final String parentId, final Bundle options) { 232 mHandler.post(new Runnable() { 233 @Override 234 public void run() { 235 for (IBinder binder : mConnections.keySet()) { 236 ConnectionRecord connection = mConnections.get(binder); 237 List<Pair<IBinder, Bundle>> callbackList = 238 connection.subscriptions.get(parentId); 239 if (callbackList != null) { 240 for (Pair<IBinder, Bundle> callback : callbackList) { 241 if (MediaBrowserCompatUtils.hasDuplicatedItems( 242 options, callback.second)) { 243 performLoadChildren(parentId, connection, callback.second, 244 options); 245 } 246 } 247 } 248 } 249 } 250 }); 251 } 252 253 @Override 254 public Bundle getBrowserRootHints() { 255 if (mCurConnection == null) { 256 throw new IllegalStateException("This should be called inside of onLoadChildren," 257 + " onLoadItem, onSearch, or onCustomAction methods"); 258 } 259 return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints); 260 } 261 262 @Override 263 public RemoteUserInfo getCurrentBrowserInfo() { 264 if (mCurConnection == null) { 265 throw new IllegalStateException("This should be called inside of onLoadChildren," 266 + " onLoadItem, onSearch, or onCustomAction methods"); 267 } 268 return mCurConnection.browserInfo; 269 } 270 271 @Override 272 public List<RemoteUserInfo> getSubscribingBrowsers(String parentId) { 273 List<RemoteUserInfo> result = new ArrayList<>(); 274 for (IBinder binder : mConnections.keySet()) { 275 ConnectionRecord connection = mConnections.get(binder); 276 List<Pair<IBinder, Bundle>> callbackList = 277 connection.subscriptions.get(parentId); 278 if (callbackList != null) { 279 result.add(connection.browserInfo); 280 } 281 } 282 return result; 283 } 284 } 285 286 @RequiresApi(21) 287 class MediaBrowserServiceImplApi21 implements MediaBrowserServiceImpl, 288 MediaBrowserServiceCompatApi21.ServiceCompatProxy { 289 final List<Bundle> mRootExtrasList = new ArrayList<>(); 290 Object mServiceObj; 291 Messenger mMessenger; 292 293 @Override 294 public void onCreate() { 295 mServiceObj = MediaBrowserServiceCompatApi21.createService( 296 MediaBrowserServiceCompat.this, this); 297 MediaBrowserServiceCompatApi21.onCreate(mServiceObj); 298 } 299 300 @Override 301 public IBinder onBind(Intent intent) { 302 return MediaBrowserServiceCompatApi21.onBind(mServiceObj, intent); 303 } 304 305 @Override 306 public void setSessionToken(final MediaSessionCompat.Token token) { 307 mHandler.postOrRun(new Runnable() { 308 @Override 309 public void run() { 310 if (!mRootExtrasList.isEmpty()) { 311 IMediaSession extraBinder = token.getExtraBinder(); 312 if (extraBinder != null) { 313 for (Bundle rootExtras : mRootExtrasList) { 314 BundleCompat.putBinder(rootExtras, EXTRA_SESSION_BINDER, 315 extraBinder.asBinder()); 316 } 317 } 318 mRootExtrasList.clear(); 319 } 320 MediaBrowserServiceCompatApi21.setSessionToken(mServiceObj, token.getToken()); 321 } 322 }); 323 } 324 325 @Override 326 public void notifyChildrenChanged(final String parentId, final Bundle options) { 327 notifyChildrenChangedForFramework(parentId, options); 328 notifyChildrenChangedForCompat(parentId, options); 329 } 330 331 @Override 332 public MediaBrowserServiceCompatApi21.BrowserRoot onGetRoot( 333 String clientPackageName, int clientUid, Bundle rootHints) { 334 Bundle rootExtras = null; 335 if (rootHints != null && rootHints.getInt(EXTRA_CLIENT_VERSION, 0) != 0) { 336 rootHints.remove(EXTRA_CLIENT_VERSION); 337 mMessenger = new Messenger(mHandler); 338 rootExtras = new Bundle(); 339 rootExtras.putInt(EXTRA_SERVICE_VERSION, SERVICE_VERSION_CURRENT); 340 BundleCompat.putBinder(rootExtras, EXTRA_MESSENGER_BINDER, mMessenger.getBinder()); 341 if (mSession != null) { 342 IMediaSession extraBinder = mSession.getExtraBinder(); 343 BundleCompat.putBinder(rootExtras, EXTRA_SESSION_BINDER, 344 extraBinder == null ? null : extraBinder.asBinder()); 345 } else { 346 mRootExtrasList.add(rootExtras); 347 } 348 } 349 // We aren't sure whether this connection request would be accepted. 350 // Temporarily set mCurConnection just to make getCurrentBrowserInfo() working. 351 mCurConnection = new ConnectionRecord(clientPackageName, -1, clientUid, rootHints, 352 null); 353 BrowserRoot root = MediaBrowserServiceCompat.this.onGetRoot( 354 clientPackageName, clientUid, rootHints); 355 mCurConnection = null; 356 if (root == null) { 357 return null; 358 } 359 if (rootExtras == null) { 360 rootExtras = root.getExtras(); 361 } else if (root.getExtras() != null) { 362 rootExtras.putAll(root.getExtras()); 363 } 364 return new MediaBrowserServiceCompatApi21.BrowserRoot( 365 root.getRootId(), rootExtras); 366 } 367 368 @Override 369 public void onLoadChildren(String parentId, 370 final MediaBrowserServiceCompatApi21.ResultWrapper<List<Parcel>> resultWrapper) { 371 final Result<List<MediaBrowserCompat.MediaItem>> result 372 = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) { 373 @Override 374 void onResultSent(List<MediaBrowserCompat.MediaItem> list) { 375 List<Parcel> parcelList = null; 376 if (list != null) { 377 parcelList = new ArrayList<>(); 378 for (MediaBrowserCompat.MediaItem item : list) { 379 Parcel parcel = Parcel.obtain(); 380 item.writeToParcel(parcel, 0); 381 parcelList.add(parcel); 382 } 383 } 384 resultWrapper.sendResult(parcelList); 385 } 386 387 @Override 388 public void detach() { 389 resultWrapper.detach(); 390 } 391 }; 392 MediaBrowserServiceCompat.this.onLoadChildren(parentId, result); 393 } 394 395 @Override 396 public List<RemoteUserInfo> getSubscribingBrowsers(String parentId) { 397 List<RemoteUserInfo> result = new ArrayList<>(); 398 for (IBinder binder : mConnections.keySet()) { 399 ConnectionRecord connection = mConnections.get(binder); 400 List<Pair<IBinder, Bundle>> callbackList = 401 connection.subscriptions.get(parentId); 402 if (callbackList != null) { 403 result.add(connection.browserInfo); 404 } 405 } 406 return result; 407 } 408 409 void notifyChildrenChangedForFramework(final String parentId, final Bundle options) { 410 MediaBrowserServiceCompatApi21.notifyChildrenChanged(mServiceObj, parentId); 411 } 412 413 void notifyChildrenChangedForCompat(final String parentId, final Bundle options) { 414 mHandler.post(new Runnable() { 415 @Override 416 public void run() { 417 for (IBinder binder : mConnections.keySet()) { 418 ConnectionRecord connection = mConnections.get(binder); 419 List<Pair<IBinder, Bundle>> callbackList = 420 connection.subscriptions.get(parentId); 421 if (callbackList != null) { 422 for (Pair<IBinder, Bundle> callback : callbackList) { 423 if (MediaBrowserCompatUtils.hasDuplicatedItems( 424 options, callback.second)) { 425 performLoadChildren(parentId, connection, callback.second, 426 options); 427 } 428 } 429 } 430 } 431 } 432 }); 433 } 434 435 @Override 436 public Bundle getBrowserRootHints() { 437 if (mMessenger == null) { 438 // TODO: Handle getBrowserRootHints when connected with framework MediaBrowser. 439 return null; 440 } 441 if (mCurConnection == null) { 442 throw new IllegalStateException("This should be called inside of onGetRoot," 443 + " onLoadChildren, onLoadItem, onSearch, or onCustomAction methods"); 444 } 445 return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints); 446 } 447 448 @Override 449 public RemoteUserInfo getCurrentBrowserInfo() { 450 if (mCurConnection == null) { 451 throw new IllegalStateException("This should be called inside of onGetRoot," 452 + " onLoadChildren, onLoadItem, onSearch, or onCustomAction methods"); 453 } 454 return mCurConnection.browserInfo; 455 } 456 } 457 458 @RequiresApi(23) 459 class MediaBrowserServiceImplApi23 extends MediaBrowserServiceImplApi21 implements 460 MediaBrowserServiceCompatApi23.ServiceCompatProxy { 461 @Override 462 public void onCreate() { 463 mServiceObj = MediaBrowserServiceCompatApi23.createService( 464 MediaBrowserServiceCompat.this, this); 465 MediaBrowserServiceCompatApi21.onCreate(mServiceObj); 466 } 467 468 @Override 469 public void onLoadItem(String itemId, 470 final MediaBrowserServiceCompatApi21.ResultWrapper<Parcel> resultWrapper) { 471 final Result<MediaBrowserCompat.MediaItem> result 472 = new Result<MediaBrowserCompat.MediaItem>(itemId) { 473 @Override 474 void onResultSent(MediaBrowserCompat.MediaItem item) { 475 if (item == null) { 476 resultWrapper.sendResult(null); 477 } else { 478 Parcel parcelItem = Parcel.obtain(); 479 item.writeToParcel(parcelItem, 0); 480 resultWrapper.sendResult(parcelItem); 481 } 482 } 483 484 @Override 485 public void detach() { 486 resultWrapper.detach(); 487 } 488 }; 489 MediaBrowserServiceCompat.this.onLoadItem(itemId, result); 490 } 491 } 492 493 @RequiresApi(26) 494 class MediaBrowserServiceImplApi26 extends MediaBrowserServiceImplApi23 implements 495 MediaBrowserServiceCompatApi26.ServiceCompatProxy { 496 @Override 497 public void onCreate() { 498 mServiceObj = MediaBrowserServiceCompatApi26.createService( 499 MediaBrowserServiceCompat.this, this); 500 MediaBrowserServiceCompatApi21.onCreate(mServiceObj); 501 } 502 503 @Override 504 public void onLoadChildren(String parentId, 505 final MediaBrowserServiceCompatApi26.ResultWrapper resultWrapper, Bundle options) { 506 final Result<List<MediaBrowserCompat.MediaItem>> result 507 = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) { 508 @Override 509 void onResultSent(List<MediaBrowserCompat.MediaItem> list) { 510 List<Parcel> parcelList = null; 511 if (list != null) { 512 parcelList = new ArrayList<>(); 513 for (MediaBrowserCompat.MediaItem item : list) { 514 Parcel parcel = Parcel.obtain(); 515 item.writeToParcel(parcel, 0); 516 parcelList.add(parcel); 517 } 518 } 519 resultWrapper.sendResult(parcelList, getFlags()); 520 } 521 522 @Override 523 public void detach() { 524 resultWrapper.detach(); 525 } 526 }; 527 MediaBrowserServiceCompat.this.onLoadChildren(parentId, result, options); 528 } 529 530 @Override 531 public Bundle getBrowserRootHints() { 532 // mCurConnection is not null when EXTRA_MESSENGER_BINDER is used. 533 if (mCurConnection != null) { 534 return mCurConnection.rootHints == null ? null 535 : new Bundle(mCurConnection.rootHints); 536 } 537 return MediaBrowserServiceCompatApi26.getBrowserRootHints(mServiceObj); 538 } 539 540 @Override 541 void notifyChildrenChangedForFramework(final String parentId, final Bundle options) { 542 if (options != null) { 543 MediaBrowserServiceCompatApi26.notifyChildrenChanged(mServiceObj, parentId, 544 options); 545 } else { 546 super.notifyChildrenChangedForFramework(parentId, options); 547 } 548 } 549 } 550 551 @RequiresApi(28) 552 class MediaBrowserServiceImplApi28 extends MediaBrowserServiceImplApi26 { 553 @Override 554 public RemoteUserInfo getCurrentBrowserInfo() { 555 // mCurConnection is not null when EXTRA_MESSENGER_BINDER is used. 556 if (mCurConnection != null) { 557 return mCurConnection.browserInfo; 558 } 559 android.media.session.MediaSessionManager.RemoteUserInfo userInfoObj = 560 ((MediaBrowserService) mServiceObj).getCurrentBrowserInfo(); 561 return new RemoteUserInfo( 562 userInfoObj.getPackageName(), userInfoObj.getPid(), userInfoObj.getUid()); 563 } 564 } 565 566 private final class ServiceHandler extends Handler { 567 private final ServiceBinderImpl mServiceBinderImpl = new ServiceBinderImpl(); 568 569 ServiceHandler() { 570 } 571 572 @Override 573 public void handleMessage(Message msg) { 574 Bundle data = msg.getData(); 575 switch (msg.what) { 576 case CLIENT_MSG_CONNECT: 577 mServiceBinderImpl.connect(data.getString(DATA_PACKAGE_NAME), 578 data.getInt(DATA_CALLING_PID), data.getInt(DATA_CALLING_UID), 579 data.getBundle(DATA_ROOT_HINTS), 580 new ServiceCallbacksCompat(msg.replyTo)); 581 break; 582 case CLIENT_MSG_DISCONNECT: 583 mServiceBinderImpl.disconnect(new ServiceCallbacksCompat(msg.replyTo)); 584 break; 585 case CLIENT_MSG_ADD_SUBSCRIPTION: 586 mServiceBinderImpl.addSubscription(data.getString(DATA_MEDIA_ITEM_ID), 587 BundleCompat.getBinder(data, DATA_CALLBACK_TOKEN), 588 data.getBundle(DATA_OPTIONS), 589 new ServiceCallbacksCompat(msg.replyTo)); 590 break; 591 case CLIENT_MSG_REMOVE_SUBSCRIPTION: 592 mServiceBinderImpl.removeSubscription(data.getString(DATA_MEDIA_ITEM_ID), 593 BundleCompat.getBinder(data, DATA_CALLBACK_TOKEN), 594 new ServiceCallbacksCompat(msg.replyTo)); 595 break; 596 case CLIENT_MSG_GET_MEDIA_ITEM: 597 mServiceBinderImpl.getMediaItem(data.getString(DATA_MEDIA_ITEM_ID), 598 (ResultReceiver) data.getParcelable(DATA_RESULT_RECEIVER), 599 new ServiceCallbacksCompat(msg.replyTo)); 600 break; 601 case CLIENT_MSG_REGISTER_CALLBACK_MESSENGER: 602 mServiceBinderImpl.registerCallbacks(new ServiceCallbacksCompat(msg.replyTo), 603 data.getString(DATA_PACKAGE_NAME), data.getInt(DATA_CALLING_PID), 604 data.getInt(DATA_CALLING_UID), data.getBundle(DATA_ROOT_HINTS)); 605 break; 606 case CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER: 607 mServiceBinderImpl.unregisterCallbacks(new ServiceCallbacksCompat(msg.replyTo)); 608 break; 609 case CLIENT_MSG_SEARCH: 610 mServiceBinderImpl.search(data.getString(DATA_SEARCH_QUERY), 611 data.getBundle(DATA_SEARCH_EXTRAS), 612 (ResultReceiver) data.getParcelable(DATA_RESULT_RECEIVER), 613 new ServiceCallbacksCompat(msg.replyTo)); 614 break; 615 case CLIENT_MSG_SEND_CUSTOM_ACTION: 616 mServiceBinderImpl.sendCustomAction(data.getString(DATA_CUSTOM_ACTION), 617 data.getBundle(DATA_CUSTOM_ACTION_EXTRAS), 618 (ResultReceiver) data.getParcelable(DATA_RESULT_RECEIVER), 619 new ServiceCallbacksCompat(msg.replyTo)); 620 break; 621 default: 622 Log.w(TAG, "Unhandled message: " + msg 623 + "\n Service version: " + SERVICE_VERSION_CURRENT 624 + "\n Client version: " + msg.arg1); 625 } 626 } 627 628 @Override 629 public boolean sendMessageAtTime(Message msg, long uptimeMillis) { 630 // Binder.getCallingUid() in handleMessage will return the uid of this process. 631 // In order to get the right calling uid, Binder.getCallingUid() should be called here. 632 Bundle data = msg.getData(); 633 data.setClassLoader(MediaBrowserCompat.class.getClassLoader()); 634 data.putInt(DATA_CALLING_UID, Binder.getCallingUid()); 635 data.putInt(DATA_CALLING_PID, Binder.getCallingPid()); 636 return super.sendMessageAtTime(msg, uptimeMillis); 637 } 638 639 public void postOrRun(Runnable r) { 640 if (Thread.currentThread() == getLooper().getThread()) { 641 r.run(); 642 } else { 643 post(r); 644 } 645 } 646 } 647 648 /** 649 * All the info about a connection. 650 */ 651 private class ConnectionRecord implements IBinder.DeathRecipient { 652 public final String pkg; 653 public final int pid; 654 public final int uid; 655 public final RemoteUserInfo browserInfo; 656 public final Bundle rootHints; 657 public final ServiceCallbacks callbacks; 658 public final HashMap<String, List<Pair<IBinder, Bundle>>> subscriptions = new HashMap<>(); 659 public BrowserRoot root; 660 661 ConnectionRecord(String pkg, int pid, int uid, Bundle rootHints, 662 ServiceCallbacks callback) { 663 this.pkg = pkg; 664 this.pid = pid; 665 this.uid = uid; 666 this.browserInfo = new RemoteUserInfo(pkg, pid, uid); 667 this.rootHints = rootHints; 668 this.callbacks = callback; 669 } 670 671 @Override 672 public void binderDied() { 673 mHandler.post(new Runnable() { 674 @Override 675 public void run() { 676 mConnections.remove(callbacks.asBinder()); 677 } 678 }); 679 } 680 } 681 682 /** 683 * Completion handler for asynchronous callback methods in {@link MediaBrowserServiceCompat}. 684 * <p> 685 * Each of the methods that takes one of these to send the result must call either 686 * {@link #sendResult} or {@link #sendError} to respond to the caller with the given results or 687 * errors. If those functions return without calling {@link #sendResult} or {@link #sendError}, 688 * they must instead call {@link #detach} before returning, and then may call 689 * {@link #sendResult} or {@link #sendError} when they are done. If {@link #sendResult}, 690 * {@link #sendError}, or {@link #detach} is called twice, an exception will be thrown. 691 * </p><p> 692 * Those functions might also want to call {@link #sendProgressUpdate} to send interim updates 693 * to the caller. If it is called after calling {@link #sendResult} or {@link #sendError}, an 694 * exception will be thrown. 695 * </p> 696 * 697 * @see MediaBrowserServiceCompat#onLoadChildren 698 * @see MediaBrowserServiceCompat#onLoadItem 699 * @see MediaBrowserServiceCompat#onSearch 700 * @see MediaBrowserServiceCompat#onCustomAction 701 */ 702 public static class Result<T> { 703 private final Object mDebug; 704 private boolean mDetachCalled; 705 private boolean mSendResultCalled; 706 private boolean mSendProgressUpdateCalled; 707 private boolean mSendErrorCalled; 708 private int mFlags; 709 710 Result(Object debug) { 711 mDebug = debug; 712 } 713 714 /** 715 * Send the result back to the caller. 716 */ 717 public void sendResult(T result) { 718 if (mSendResultCalled || mSendErrorCalled) { 719 throw new IllegalStateException("sendResult() called when either sendResult() or " 720 + "sendError() had already been called for: " + mDebug); 721 } 722 mSendResultCalled = true; 723 onResultSent(result); 724 } 725 726 /** 727 * Send an interim update to the caller. This method is supported only when it is used in 728 * {@link #onCustomAction}. 729 * 730 * @param extras A bundle that contains extra data. 731 */ 732 public void sendProgressUpdate(Bundle extras) { 733 if (mSendResultCalled || mSendErrorCalled) { 734 throw new IllegalStateException("sendProgressUpdate() called when either " 735 + "sendResult() or sendError() had already been called for: " + mDebug); 736 } 737 checkExtraFields(extras); 738 mSendProgressUpdateCalled = true; 739 onProgressUpdateSent(extras); 740 } 741 742 /** 743 * Notify the caller of a failure. This is supported only when it is used in 744 * {@link #onCustomAction}. 745 * 746 * @param extras A bundle that contains extra data. 747 */ 748 public void sendError(Bundle extras) { 749 if (mSendResultCalled || mSendErrorCalled) { 750 throw new IllegalStateException("sendError() called when either sendResult() or " 751 + "sendError() had already been called for: " + mDebug); 752 } 753 mSendErrorCalled = true; 754 onErrorSent(extras); 755 } 756 757 /** 758 * Detach this message from the current thread and allow the {@link #sendResult} 759 * call to happen later. 760 */ 761 public void detach() { 762 if (mDetachCalled) { 763 throw new IllegalStateException("detach() called when detach() had already" 764 + " been called for: " + mDebug); 765 } 766 if (mSendResultCalled) { 767 throw new IllegalStateException("detach() called when sendResult() had already" 768 + " been called for: " + mDebug); 769 } 770 if (mSendErrorCalled) { 771 throw new IllegalStateException("detach() called when sendError() had already" 772 + " been called for: " + mDebug); 773 } 774 mDetachCalled = true; 775 } 776 777 boolean isDone() { 778 return mDetachCalled || mSendResultCalled || mSendErrorCalled; 779 } 780 781 void setFlags(@ResultFlags int flags) { 782 mFlags = flags; 783 } 784 785 int getFlags() { 786 return mFlags; 787 } 788 789 /** 790 * Called when the result is sent, after assertions about not being called twice have 791 * happened. 792 */ 793 void onResultSent(T result) { 794 } 795 796 /** 797 * Called when an interim update is sent. 798 */ 799 void onProgressUpdateSent(Bundle extras) { 800 throw new UnsupportedOperationException("It is not supported to send an interim update " 801 + "for " + mDebug); 802 } 803 804 /** 805 * Called when an error is sent, after assertions about not being called twice have 806 * happened. 807 */ 808 void onErrorSent(Bundle extras) { 809 throw new UnsupportedOperationException("It is not supported to send an error for " 810 + mDebug); 811 } 812 813 private void checkExtraFields(Bundle extras) { 814 if (extras == null) { 815 return; 816 } 817 if (extras.containsKey(MediaBrowserCompat.EXTRA_DOWNLOAD_PROGRESS)) { 818 float value = extras.getFloat(MediaBrowserCompat.EXTRA_DOWNLOAD_PROGRESS); 819 if (value < -EPSILON || value > 1.0f + EPSILON) { 820 throw new IllegalArgumentException("The value of the EXTRA_DOWNLOAD_PROGRESS " 821 + "field must be a float number within [0.0, 1.0]."); 822 } 823 } 824 } 825 } 826 827 private class ServiceBinderImpl { 828 ServiceBinderImpl() { 829 } 830 831 public void connect(final String pkg, final int pid, final int uid, final Bundle rootHints, 832 final ServiceCallbacks callbacks) { 833 834 if (!isValidPackage(pkg, uid)) { 835 throw new IllegalArgumentException("Package/uid mismatch: uid=" + uid 836 + " package=" + pkg); 837 } 838 839 mHandler.postOrRun(new Runnable() { 840 @Override 841 public void run() { 842 final IBinder b = callbacks.asBinder(); 843 844 // Clear out the old subscriptions. We are getting new ones. 845 mConnections.remove(b); 846 847 final ConnectionRecord connection = new ConnectionRecord(pkg, pid, uid, 848 rootHints, callbacks); 849 mCurConnection = connection; 850 connection.root = MediaBrowserServiceCompat.this.onGetRoot(pkg, uid, rootHints); 851 mCurConnection = null; 852 853 // If they didn't return something, don't allow this client. 854 if (connection.root == null) { 855 Log.i(TAG, "No root for client " + pkg + " from service " 856 + getClass().getName()); 857 try { 858 callbacks.onConnectFailed(); 859 } catch (RemoteException ex) { 860 Log.w(TAG, "Calling onConnectFailed() failed. Ignoring. " 861 + "pkg=" + pkg); 862 } 863 } else { 864 try { 865 mConnections.put(b, connection); 866 b.linkToDeath(connection, 0); 867 if (mSession != null) { 868 callbacks.onConnect(connection.root.getRootId(), 869 mSession, connection.root.getExtras()); 870 } 871 } catch (RemoteException ex) { 872 Log.w(TAG, "Calling onConnect() failed. Dropping client. " 873 + "pkg=" + pkg); 874 mConnections.remove(b); 875 } 876 } 877 } 878 }); 879 } 880 881 public void disconnect(final ServiceCallbacks callbacks) { 882 mHandler.postOrRun(new Runnable() { 883 @Override 884 public void run() { 885 final IBinder b = callbacks.asBinder(); 886 887 // Clear out the old subscriptions. We are getting new ones. 888 final ConnectionRecord old = mConnections.remove(b); 889 if (old != null) { 890 // TODO 891 old.callbacks.asBinder().unlinkToDeath(old, 0); 892 } 893 } 894 }); 895 } 896 897 public void addSubscription(final String id, final IBinder token, final Bundle options, 898 final ServiceCallbacks callbacks) { 899 mHandler.postOrRun(new Runnable() { 900 @Override 901 public void run() { 902 final IBinder b = callbacks.asBinder(); 903 904 // Get the record for the connection 905 final ConnectionRecord connection = mConnections.get(b); 906 if (connection == null) { 907 Log.w(TAG, "addSubscription for callback that isn't registered id=" 908 + id); 909 return; 910 } 911 912 MediaBrowserServiceCompat.this.addSubscription(id, connection, token, options); 913 } 914 }); 915 } 916 917 public void removeSubscription(final String id, final IBinder token, 918 final ServiceCallbacks callbacks) { 919 mHandler.postOrRun(new Runnable() { 920 @Override 921 public void run() { 922 final IBinder b = callbacks.asBinder(); 923 924 ConnectionRecord connection = mConnections.get(b); 925 if (connection == null) { 926 Log.w(TAG, "removeSubscription for callback that isn't registered id=" 927 + id); 928 return; 929 } 930 if (!MediaBrowserServiceCompat.this.removeSubscription( 931 id, connection, token)) { 932 Log.w(TAG, "removeSubscription called for " + id 933 + " which is not subscribed"); 934 } 935 } 936 }); 937 } 938 939 public void getMediaItem(final String mediaId, final ResultReceiver receiver, 940 final ServiceCallbacks callbacks) { 941 if (TextUtils.isEmpty(mediaId) || receiver == null) { 942 return; 943 } 944 945 mHandler.postOrRun(new Runnable() { 946 @Override 947 public void run() { 948 final IBinder b = callbacks.asBinder(); 949 950 ConnectionRecord connection = mConnections.get(b); 951 if (connection == null) { 952 Log.w(TAG, "getMediaItem for callback that isn't registered id=" + mediaId); 953 return; 954 } 955 performLoadItem(mediaId, connection, receiver); 956 } 957 }); 958 } 959 960 // Used when {@link MediaBrowserProtocol#EXTRA_MESSENGER_BINDER} is used. 961 public void registerCallbacks(final ServiceCallbacks callbacks, final String pkg, 962 final int pid, final int uid, final Bundle rootHints) { 963 mHandler.postOrRun(new Runnable() { 964 @Override 965 public void run() { 966 final IBinder b = callbacks.asBinder(); 967 // Clear out the old subscriptions. We are getting new ones. 968 mConnections.remove(b); 969 970 final ConnectionRecord connection = new ConnectionRecord(pkg, pid, uid, 971 rootHints, callbacks); 972 mConnections.put(b, connection); 973 try { 974 b.linkToDeath(connection, 0); 975 } catch (RemoteException e) { 976 Log.w(TAG, "IBinder is already dead."); 977 } 978 } 979 }); 980 } 981 982 // Used when {@link MediaBrowserProtocol#EXTRA_MESSENGER_BINDER} is used. 983 public void unregisterCallbacks(final ServiceCallbacks callbacks) { 984 mHandler.postOrRun(new Runnable() { 985 @Override 986 public void run() { 987 final IBinder b = callbacks.asBinder(); 988 ConnectionRecord old = mConnections.remove(b); 989 if (old != null) { 990 b.unlinkToDeath(old, 0); 991 } 992 } 993 }); 994 } 995 996 public void search(final String query, final Bundle extras, final ResultReceiver receiver, 997 final ServiceCallbacks callbacks) { 998 if (TextUtils.isEmpty(query) || receiver == null) { 999 return; 1000 } 1001 1002 mHandler.postOrRun(new Runnable() { 1003 @Override 1004 public void run() { 1005 final IBinder b = callbacks.asBinder(); 1006 1007 ConnectionRecord connection = mConnections.get(b); 1008 if (connection == null) { 1009 Log.w(TAG, "search for callback that isn't registered query=" + query); 1010 return; 1011 } 1012 performSearch(query, extras, connection, receiver); 1013 } 1014 }); 1015 } 1016 1017 public void sendCustomAction(final String action, final Bundle extras, 1018 final ResultReceiver receiver, final ServiceCallbacks callbacks) { 1019 if (TextUtils.isEmpty(action) || receiver == null) { 1020 return; 1021 } 1022 1023 mHandler.postOrRun(new Runnable() { 1024 @Override 1025 public void run() { 1026 final IBinder b = callbacks.asBinder(); 1027 1028 ConnectionRecord connection = mConnections.get(b); 1029 if (connection == null) { 1030 Log.w(TAG, "sendCustomAction for callback that isn't registered action=" 1031 + action + ", extras=" + extras); 1032 return; 1033 } 1034 performCustomAction(action, extras, connection, receiver); 1035 } 1036 }); 1037 } 1038 } 1039 1040 private interface ServiceCallbacks { 1041 IBinder asBinder(); 1042 void onConnect(String root, MediaSessionCompat.Token session, Bundle extras) 1043 throws RemoteException; 1044 void onConnectFailed() throws RemoteException; 1045 void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, Bundle options, 1046 Bundle notifyChildrenChangedOptions) throws RemoteException; 1047 } 1048 1049 private static class ServiceCallbacksCompat implements ServiceCallbacks { 1050 final Messenger mCallbacks; 1051 1052 ServiceCallbacksCompat(Messenger callbacks) { 1053 mCallbacks = callbacks; 1054 } 1055 1056 @Override 1057 public IBinder asBinder() { 1058 return mCallbacks.getBinder(); 1059 } 1060 1061 @Override 1062 public void onConnect(String root, MediaSessionCompat.Token session, Bundle extras) 1063 throws RemoteException { 1064 if (extras == null) { 1065 extras = new Bundle(); 1066 } 1067 extras.putInt(EXTRA_SERVICE_VERSION, SERVICE_VERSION_CURRENT); 1068 Bundle data = new Bundle(); 1069 data.putString(DATA_MEDIA_ITEM_ID, root); 1070 data.putParcelable(DATA_MEDIA_SESSION_TOKEN, session); 1071 data.putBundle(DATA_ROOT_HINTS, extras); 1072 sendRequest(SERVICE_MSG_ON_CONNECT, data); 1073 } 1074 1075 @Override 1076 public void onConnectFailed() throws RemoteException { 1077 sendRequest(SERVICE_MSG_ON_CONNECT_FAILED, null); 1078 } 1079 1080 @Override 1081 public void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, 1082 Bundle options, Bundle notifyChildrenChangedOptions) throws RemoteException { 1083 Bundle data = new Bundle(); 1084 data.putString(DATA_MEDIA_ITEM_ID, mediaId); 1085 data.putBundle(DATA_OPTIONS, options); 1086 data.putBundle(DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS, notifyChildrenChangedOptions); 1087 if (list != null) { 1088 data.putParcelableArrayList(DATA_MEDIA_ITEM_LIST, 1089 list instanceof ArrayList ? (ArrayList) list : new ArrayList<>(list)); 1090 } 1091 sendRequest(SERVICE_MSG_ON_LOAD_CHILDREN, data); 1092 } 1093 1094 private void sendRequest(int what, Bundle data) throws RemoteException { 1095 Message msg = Message.obtain(); 1096 msg.what = what; 1097 msg.arg1 = SERVICE_VERSION_CURRENT; 1098 msg.setData(data); 1099 mCallbacks.send(msg); 1100 } 1101 } 1102 1103 /** 1104 * Attaches to the base context. This method is added to change the visibility of 1105 * {@link Service#attachBaseContext(Context)}. 1106 * <p> 1107 * Note that we cannot simply override {@link Service#attachBaseContext(Context)} and hide it 1108 * because lint checks considers the overriden method as the new public API that needs update 1109 * of current.txt. 1110 * 1111 * @hide 1112 */ 1113 @RestrictTo(LIBRARY) 1114 public void attachToBaseContext(Context base) { 1115 attachBaseContext(base); 1116 } 1117 1118 @Override 1119 public void onCreate() { 1120 super.onCreate(); 1121 if (Build.VERSION.SDK_INT >= 28) { 1122 mImpl = new MediaBrowserServiceImplApi28(); 1123 } else if (Build.VERSION.SDK_INT >= 26) { 1124 mImpl = new MediaBrowserServiceImplApi26(); 1125 } else if (Build.VERSION.SDK_INT >= 23) { 1126 mImpl = new MediaBrowserServiceImplApi23(); 1127 } else if (Build.VERSION.SDK_INT >= 21) { 1128 mImpl = new MediaBrowserServiceImplApi21(); 1129 } else { 1130 mImpl = new MediaBrowserServiceImplBase(); 1131 } 1132 mImpl.onCreate(); 1133 } 1134 1135 @Override 1136 public IBinder onBind(Intent intent) { 1137 return mImpl.onBind(intent); 1138 } 1139 1140 @Override 1141 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 1142 } 1143 1144 /** 1145 * Called to get the root information for browsing by a particular client. 1146 * <p> 1147 * The implementation should verify that the client package has permission 1148 * to access browse media information before returning the root id; it 1149 * should return null if the client is not allowed to access this 1150 * information. 1151 * </p> 1152 * 1153 * @param clientPackageName The package name of the application which is 1154 * requesting access to browse media. 1155 * @param clientUid The uid of the application which is requesting access to 1156 * browse media. 1157 * @param rootHints An optional bundle of service-specific arguments to send 1158 * to the media browse service when connecting and retrieving the 1159 * root id for browsing, or null if none. The contents of this 1160 * bundle may affect the information returned when browsing. 1161 * @return The {@link BrowserRoot} for accessing this app's content or null. 1162 * @see BrowserRoot#EXTRA_RECENT 1163 * @see BrowserRoot#EXTRA_OFFLINE 1164 * @see BrowserRoot#EXTRA_SUGGESTED 1165 */ 1166 public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName, 1167 int clientUid, @Nullable Bundle rootHints); 1168 1169 /** 1170 * Called to get information about the children of a media item. 1171 * <p> 1172 * Implementations must call {@link Result#sendResult result.sendResult} 1173 * with the list of children. If loading the children will be an expensive 1174 * operation that should be performed on another thread, 1175 * {@link Result#detach result.detach} may be called before returning from 1176 * this function, and then {@link Result#sendResult result.sendResult} 1177 * called when the loading is complete. 1178 * </p><p> 1179 * In case the media item does not have any children, call {@link Result#sendResult} 1180 * with an empty list. When the given {@code parentId} is invalid, implementations must 1181 * call {@link Result#sendResult result.sendResult} with {@code null}, which will invoke 1182 * {@link MediaBrowserCompat.SubscriptionCallback#onError}. 1183 * </p> 1184 * 1185 * @param parentId The id of the parent media item whose children are to be 1186 * queried. 1187 * @param result The Result to send the list of children to. 1188 */ 1189 public abstract void onLoadChildren(@NonNull String parentId, 1190 @NonNull Result<List<MediaBrowserCompat.MediaItem>> result); 1191 1192 /** 1193 * Called to get information about the children of a media item. 1194 * <p> 1195 * Implementations must call {@link Result#sendResult result.sendResult} 1196 * with the list of children. If loading the children will be an expensive 1197 * operation that should be performed on another thread, 1198 * {@link Result#detach result.detach} may be called before returning from 1199 * this function, and then {@link Result#sendResult result.sendResult} 1200 * called when the loading is complete. 1201 * </p><p> 1202 * In case the media item does not have any children, call {@link Result#sendResult} 1203 * with an empty list. When the given {@code parentId} is invalid, implementations must 1204 * call {@link Result#sendResult result.sendResult} with {@code null}, which will invoke 1205 * {@link MediaBrowserCompat.SubscriptionCallback#onError}. 1206 * </p> 1207 * 1208 * @param parentId The id of the parent media item whose children are to be 1209 * queried. 1210 * @param result The Result to send the list of children to. 1211 * @param options A bundle of service-specific arguments sent from the media 1212 * browse. The information returned through the result should be 1213 * affected by the contents of this bundle. 1214 */ 1215 public void onLoadChildren(@NonNull String parentId, 1216 @NonNull Result<List<MediaBrowserCompat.MediaItem>> result, @NonNull Bundle options) { 1217 // To support backward compatibility, when the implementation of MediaBrowserService doesn't 1218 // override onLoadChildren() with options, onLoadChildren() without options will be used 1219 // instead, and the options will be applied in the implementation of result.onResultSent(). 1220 result.setFlags(RESULT_FLAG_OPTION_NOT_HANDLED); 1221 onLoadChildren(parentId, result); 1222 } 1223 1224 /** 1225 * Called to get information about a specific media item. 1226 * <p> 1227 * Implementations must call {@link Result#sendResult result.sendResult}. If 1228 * loading the item will be an expensive operation {@link Result#detach 1229 * result.detach} may be called before returning from this function, and 1230 * then {@link Result#sendResult result.sendResult} called when the item has 1231 * been loaded. 1232 * </p><p> 1233 * When the given {@code itemId} is invalid, implementations must call 1234 * {@link Result#sendResult result.sendResult} with {@code null}. 1235 * </p><p> 1236 * The default implementation will invoke {@link MediaBrowserCompat.ItemCallback#onError}. 1237 * 1238 * @param itemId The id for the specific {@link MediaBrowserCompat.MediaItem}. 1239 * @param result The Result to send the item to, or null if the id is 1240 * invalid. 1241 */ 1242 public void onLoadItem(String itemId, @NonNull Result<MediaBrowserCompat.MediaItem> result) { 1243 result.setFlags(RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED); 1244 result.sendResult(null); 1245 } 1246 1247 /** 1248 * Called to get the search result. 1249 * <p> 1250 * Implementations must call {@link Result#sendResult result.sendResult}. If the search will be 1251 * an expensive operation {@link Result#detach result.detach} may be called before returning 1252 * from this function, and then {@link Result#sendResult result.sendResult} called when the 1253 * search has been completed. 1254 * </p><p> 1255 * In case there are no search results, call {@link Result#sendResult result.sendResult} with an 1256 * empty list. In case there are some errors happened, call {@link Result#sendResult 1257 * result.sendResult} with {@code null}, which will invoke {@link 1258 * MediaBrowserCompat.SearchCallback#onError}. 1259 * </p><p> 1260 * The default implementation will invoke {@link MediaBrowserCompat.SearchCallback#onError}. 1261 * </p> 1262 * 1263 * @param query The search query sent from the media browser. It contains keywords separated 1264 * by space. 1265 * @param extras The bundle of service-specific arguments sent from the media browser. 1266 * @param result The {@link Result} to send the search result. 1267 */ 1268 public void onSearch(@NonNull String query, Bundle extras, 1269 @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) { 1270 result.setFlags(RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED); 1271 result.sendResult(null); 1272 } 1273 1274 /** 1275 * Called to request a custom action to this service. 1276 * <p> 1277 * Implementations must call either {@link Result#sendResult} or {@link Result#sendError}. If 1278 * the requested custom action will be an expensive operation {@link Result#detach} may be 1279 * called before returning from this function, and then the service can send the result later 1280 * when the custom action is completed. Implementation can also call 1281 * {@link Result#sendProgressUpdate} to send an interim update to the requester. 1282 * </p><p> 1283 * If the requested custom action is not supported by this service, call 1284 * {@link Result#sendError}. The default implementation will invoke {@link Result#sendError}. 1285 * </p> 1286 * 1287 * @param action The custom action sent from the media browser. 1288 * @param extras The bundle of service-specific arguments sent from the media browser. 1289 * @param result The {@link Result} to send the result of the requested custom action. 1290 * @see MediaBrowserCompat#CUSTOM_ACTION_DOWNLOAD 1291 * @see MediaBrowserCompat#CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE 1292 */ 1293 public void onCustomAction(@NonNull String action, Bundle extras, 1294 @NonNull Result<Bundle> result) { 1295 result.sendError(null); 1296 } 1297 1298 /** 1299 * Call to set the media session. 1300 * <p> 1301 * This should be called as soon as possible during the service's startup. 1302 * It may only be called once. 1303 * 1304 * @param token The token for the service's {@link MediaSessionCompat}. 1305 */ 1306 public void setSessionToken(MediaSessionCompat.Token token) { 1307 if (token == null) { 1308 throw new IllegalArgumentException("Session token may not be null."); 1309 } 1310 if (mSession != null) { 1311 throw new IllegalStateException("The session token has already been set."); 1312 } 1313 mSession = token; 1314 mImpl.setSessionToken(token); 1315 } 1316 1317 /** 1318 * Gets the session token, or null if it has not yet been created 1319 * or if it has been destroyed. 1320 */ 1321 public @Nullable MediaSessionCompat.Token getSessionToken() { 1322 return mSession; 1323 } 1324 1325 /** 1326 * Gets the root hints sent from the currently connected {@link MediaBrowserCompat}. 1327 * The root hints are service-specific arguments included in an optional bundle sent to the 1328 * media browser service when connecting and retrieving the root id for browsing, or null if 1329 * none. The contents of this bundle may affect the information returned when browsing. 1330 * <p> 1331 * Note that this will return null when connected to {@link android.media.browse.MediaBrowser} 1332 * and running on API 23 or lower. 1333 * 1334 * @throws IllegalStateException If this method is called outside of {@link #onLoadChildren}, 1335 * {@link #onLoadItem} or {@link #onSearch}. 1336 * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_RECENT 1337 * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_OFFLINE 1338 * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_SUGGESTED 1339 */ 1340 public final Bundle getBrowserRootHints() { 1341 return mImpl.getBrowserRootHints(); 1342 } 1343 1344 /** 1345 * Gets the browser information who sent the current request. 1346 * 1347 * @throws IllegalStateException If this method is called outside of {@link #onGetRoot} or 1348 * {@link #onLoadChildren} or {@link #onLoadItem}. 1349 * @see MediaSessionManager#isTrustedForMediaControl(RemoteUserInfo) 1350 */ 1351 public final @NonNull RemoteUserInfo getCurrentBrowserInfo() { 1352 return mImpl.getCurrentBrowserInfo(); 1353 } 1354 1355 /** 1356 * Notifies all connected media browsers that the children of 1357 * the specified parent id have changed in some way. 1358 * This will cause browsers to fetch subscribed content again. 1359 * 1360 * @param parentId The id of the parent media item whose 1361 * children changed. 1362 */ 1363 public void notifyChildrenChanged(@NonNull String parentId) { 1364 if (parentId == null) { 1365 throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged"); 1366 } 1367 mImpl.notifyChildrenChanged(parentId, null); 1368 } 1369 1370 /** 1371 * Notifies all connected media browsers that the children of 1372 * the specified parent id have changed in some way. 1373 * This will cause browsers to fetch subscribed content again. 1374 * 1375 * @param parentId The id of the parent media item whose 1376 * children changed. 1377 * @param options A bundle of service-specific arguments to send 1378 * to the media browse. The contents of this bundle may 1379 * contain the information about the change. 1380 */ 1381 public void notifyChildrenChanged(@NonNull String parentId, @NonNull Bundle options) { 1382 if (parentId == null) { 1383 throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged"); 1384 } 1385 if (options == null) { 1386 throw new IllegalArgumentException("options cannot be null in notifyChildrenChanged"); 1387 } 1388 mImpl.notifyChildrenChanged(parentId, options); 1389 } 1390 1391 /** 1392 * Gets {@link RemoteUserInfo} of all browsers which are subscribing to the given parentId. 1393 * @hide 1394 */ 1395 @RestrictTo(LIBRARY) 1396 public @NonNull List<RemoteUserInfo> getSubscribingBrowsers(@NonNull String parentId) { 1397 if (parentId == null) { 1398 throw new IllegalArgumentException("parentId cannot be null in getSubscribingBrowsers"); 1399 } 1400 return mImpl.getSubscribingBrowsers(parentId); 1401 } 1402 1403 /** 1404 * Return whether the given package is one of the ones that is owned by the uid. 1405 */ 1406 boolean isValidPackage(String pkg, int uid) { 1407 if (pkg == null) { 1408 return false; 1409 } 1410 final PackageManager pm = getPackageManager(); 1411 final String[] packages = pm.getPackagesForUid(uid); 1412 final int N = packages.length; 1413 for (int i=0; i<N; i++) { 1414 if (packages[i].equals(pkg)) { 1415 return true; 1416 } 1417 } 1418 return false; 1419 } 1420 1421 /** 1422 * Save the subscription and if it is a new subscription send the results. 1423 */ 1424 void addSubscription(String id, ConnectionRecord connection, IBinder token, 1425 Bundle options) { 1426 // Save the subscription 1427 List<Pair<IBinder, Bundle>> callbackList = connection.subscriptions.get(id); 1428 if (callbackList == null) { 1429 callbackList = new ArrayList<>(); 1430 } 1431 for (Pair<IBinder, Bundle> callback : callbackList) { 1432 if (token == callback.first 1433 && MediaBrowserCompatUtils.areSameOptions(options, callback.second)) { 1434 return; 1435 } 1436 } 1437 callbackList.add(new Pair<>(token, options)); 1438 connection.subscriptions.put(id, callbackList); 1439 // send the results 1440 performLoadChildren(id, connection, options, null); 1441 } 1442 1443 /** 1444 * Remove the subscription. 1445 */ 1446 boolean removeSubscription(String id, ConnectionRecord connection, IBinder token) { 1447 if (token == null) { 1448 return connection.subscriptions.remove(id) != null; 1449 } 1450 boolean removed = false; 1451 List<Pair<IBinder, Bundle>> callbackList = connection.subscriptions.get(id); 1452 if (callbackList != null) { 1453 Iterator<Pair<IBinder, Bundle>> iter = callbackList.iterator(); 1454 while (iter.hasNext()){ 1455 if (token == iter.next().first) { 1456 removed = true; 1457 iter.remove(); 1458 } 1459 } 1460 if (callbackList.size() == 0) { 1461 connection.subscriptions.remove(id); 1462 } 1463 } 1464 return removed; 1465 } 1466 1467 /** 1468 * Call onLoadChildren and then send the results back to the connection. 1469 * <p> 1470 * Callers must make sure that this connection is still connected. 1471 */ 1472 void performLoadChildren(final String parentId, final ConnectionRecord connection, 1473 final Bundle subscribeOptions, final Bundle notifyChildrenChangedOptions) { 1474 final Result<List<MediaBrowserCompat.MediaItem>> result 1475 = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) { 1476 @Override 1477 void onResultSent(List<MediaBrowserCompat.MediaItem> list) { 1478 if (mConnections.get(connection.callbacks.asBinder()) != connection) { 1479 if (DEBUG) { 1480 Log.d(TAG, "Not sending onLoadChildren result for connection that has" 1481 + " been disconnected. pkg=" + connection.pkg + " id=" + parentId); 1482 } 1483 return; 1484 } 1485 1486 List<MediaBrowserCompat.MediaItem> filteredList = 1487 (getFlags() & RESULT_FLAG_OPTION_NOT_HANDLED) != 0 1488 ? applyOptions(list, subscribeOptions) : list; 1489 try { 1490 connection.callbacks.onLoadChildren(parentId, filteredList, subscribeOptions, 1491 notifyChildrenChangedOptions); 1492 } catch (RemoteException ex) { 1493 // The other side is in the process of crashing. 1494 Log.w(TAG, "Calling onLoadChildren() failed for id=" + parentId 1495 + " package=" + connection.pkg); 1496 } 1497 } 1498 }; 1499 1500 mCurConnection = connection; 1501 if (subscribeOptions == null) { 1502 onLoadChildren(parentId, result); 1503 } else { 1504 onLoadChildren(parentId, result, subscribeOptions); 1505 } 1506 mCurConnection = null; 1507 1508 if (!result.isDone()) { 1509 throw new IllegalStateException("onLoadChildren must call detach() or sendResult()" 1510 + " before returning for package=" + connection.pkg + " id=" + parentId); 1511 } 1512 } 1513 1514 List<MediaBrowserCompat.MediaItem> applyOptions(List<MediaBrowserCompat.MediaItem> list, 1515 final Bundle options) { 1516 if (list == null) { 1517 return null; 1518 } 1519 int page = options.getInt(MediaBrowserCompat.EXTRA_PAGE, -1); 1520 int pageSize = options.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, -1); 1521 if (page == -1 && pageSize == -1) { 1522 return list; 1523 } 1524 int fromIndex = pageSize * page; 1525 int toIndex = fromIndex + pageSize; 1526 if (page < 0 || pageSize < 1 || fromIndex >= list.size()) { 1527 return Collections.EMPTY_LIST; 1528 } 1529 if (toIndex > list.size()) { 1530 toIndex = list.size(); 1531 } 1532 return list.subList(fromIndex, toIndex); 1533 } 1534 1535 void performLoadItem(String itemId, ConnectionRecord connection, 1536 final ResultReceiver receiver) { 1537 final Result<MediaBrowserCompat.MediaItem> result = 1538 new Result<MediaBrowserCompat.MediaItem>(itemId) { 1539 @Override 1540 void onResultSent(MediaBrowserCompat.MediaItem item) { 1541 if ((getFlags() & RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED) != 0) { 1542 receiver.send(RESULT_ERROR, null); 1543 return; 1544 } 1545 Bundle bundle = new Bundle(); 1546 bundle.putParcelable(KEY_MEDIA_ITEM, item); 1547 receiver.send(RESULT_OK, bundle); 1548 } 1549 }; 1550 1551 mCurConnection = connection; 1552 onLoadItem(itemId, result); 1553 mCurConnection = null; 1554 1555 if (!result.isDone()) { 1556 throw new IllegalStateException("onLoadItem must call detach() or sendResult()" 1557 + " before returning for id=" + itemId); 1558 } 1559 } 1560 1561 void performSearch(final String query, Bundle extras, ConnectionRecord connection, 1562 final ResultReceiver receiver) { 1563 final Result<List<MediaBrowserCompat.MediaItem>> result = 1564 new Result<List<MediaBrowserCompat.MediaItem>>(query) { 1565 @Override 1566 void onResultSent(List<MediaBrowserCompat.MediaItem> items) { 1567 if ((getFlags() & RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED) != 0 1568 || items == null) { 1569 receiver.send(RESULT_ERROR, null); 1570 return; 1571 } 1572 Bundle bundle = new Bundle(); 1573 bundle.putParcelableArray(KEY_SEARCH_RESULTS, 1574 items.toArray(new MediaBrowserCompat.MediaItem[0])); 1575 receiver.send(RESULT_OK, bundle); 1576 } 1577 }; 1578 1579 mCurConnection = connection; 1580 onSearch(query, extras, result); 1581 mCurConnection = null; 1582 1583 if (!result.isDone()) { 1584 throw new IllegalStateException("onSearch must call detach() or sendResult()" 1585 + " before returning for query=" + query); 1586 } 1587 } 1588 1589 void performCustomAction(final String action, Bundle extras, ConnectionRecord connection, 1590 final ResultReceiver receiver) { 1591 final Result<Bundle> result = new Result<Bundle>(action) { 1592 @Override 1593 void onResultSent(Bundle result) { 1594 receiver.send(RESULT_OK, result); 1595 } 1596 1597 @Override 1598 void onProgressUpdateSent(Bundle data) { 1599 receiver.send(RESULT_PROGRESS_UPDATE, data); 1600 } 1601 1602 @Override 1603 void onErrorSent(Bundle data) { 1604 receiver.send(RESULT_ERROR, data); 1605 } 1606 }; 1607 1608 mCurConnection = connection; 1609 onCustomAction(action, extras, result); 1610 mCurConnection = null; 1611 1612 if (!result.isDone()) { 1613 throw new IllegalStateException("onCustomAction must call detach() or sendResult() or " 1614 + "sendError() before returning for action=" + action + " extras=" 1615 + extras); 1616 } 1617 } 1618 1619 /** 1620 * Contains information that the browser service needs to send to the client 1621 * when first connected. 1622 */ 1623 public static final class BrowserRoot { 1624 /** 1625 * The lookup key for a boolean that indicates whether the browser service should return a 1626 * browser root for recently played media items. 1627 * 1628 * <p>When creating a media browser for a given media browser service, this key can be 1629 * supplied as a root hint for retrieving media items that are recently played. 1630 * If the media browser service can provide such media items, the implementation must return 1631 * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back. 1632 * 1633 * <p>The root hint may contain multiple keys. 1634 * 1635 * @see #EXTRA_OFFLINE 1636 * @see #EXTRA_SUGGESTED 1637 */ 1638 public static final String EXTRA_RECENT = "android.service.media.extra.RECENT"; 1639 1640 /** 1641 * The lookup key for a boolean that indicates whether the browser service should return a 1642 * browser root for offline media items. 1643 * 1644 * <p>When creating a media browser for a given media browser service, this key can be 1645 * supplied as a root hint for retrieving media items that are can be played without an 1646 * internet connection. 1647 * If the media browser service can provide such media items, the implementation must return 1648 * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back. 1649 * 1650 * <p>The root hint may contain multiple keys. 1651 * 1652 * @see #EXTRA_RECENT 1653 * @see #EXTRA_SUGGESTED 1654 */ 1655 public static final String EXTRA_OFFLINE = "android.service.media.extra.OFFLINE"; 1656 1657 /** 1658 * The lookup key for a boolean that indicates whether the browser service should return a 1659 * browser root for suggested media items. 1660 * 1661 * <p>When creating a media browser for a given media browser service, this key can be 1662 * supplied as a root hint for retrieving the media items suggested by the media browser 1663 * service. The list of media items passed in {@link MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded(String, List)} 1664 * is considered ordered by relevance, first being the top suggestion. 1665 * If the media browser service can provide such media items, the implementation must return 1666 * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back. 1667 * 1668 * <p>The root hint may contain multiple keys. 1669 * 1670 * @see #EXTRA_RECENT 1671 * @see #EXTRA_OFFLINE 1672 */ 1673 public static final String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED"; 1674 1675 /** 1676 * The lookup key for a string that indicates specific keywords which will be considered 1677 * when the browser service suggests media items. 1678 * 1679 * <p>When creating a media browser for a given media browser service, this key can be 1680 * supplied as a root hint together with {@link #EXTRA_SUGGESTED} for retrieving suggested 1681 * media items related with the keywords. The list of media items passed in 1682 * {@link android.media.browse.MediaBrowser.SubscriptionCallback#onChildrenLoaded(String, List)} 1683 * is considered ordered by relevance, first being the top suggestion. 1684 * If the media browser service can provide such media items, the implementation must return 1685 * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back. 1686 * 1687 * <p>The root hint may contain multiple keys. 1688 * 1689 * @see #EXTRA_RECENT 1690 * @see #EXTRA_OFFLINE 1691 * @see #EXTRA_SUGGESTED 1692 * @deprecated The search functionality is now supported by the methods 1693 * {@link MediaBrowserCompat#search} and {@link #onSearch}. Use those methods 1694 * instead. 1695 */ 1696 @Deprecated 1697 public static final String EXTRA_SUGGESTION_KEYWORDS 1698 = "android.service.media.extra.SUGGESTION_KEYWORDS"; 1699 1700 final private String mRootId; 1701 final private Bundle mExtras; 1702 1703 /** 1704 * Constructs a browser root. 1705 * @param rootId The root id for browsing. 1706 * @param extras Any extras about the browser service. 1707 */ 1708 public BrowserRoot(@NonNull String rootId, @Nullable Bundle extras) { 1709 if (rootId == null) { 1710 throw new IllegalArgumentException("The root id in BrowserRoot cannot be null. " + 1711 "Use null for BrowserRoot instead."); 1712 } 1713 mRootId = rootId; 1714 mExtras = extras; 1715 } 1716 1717 /** 1718 * Gets the root id for browsing. 1719 */ 1720 public String getRootId() { 1721 return mRootId; 1722 } 1723 1724 /** 1725 * Gets any extras about the browser service. 1726 */ 1727 public Bundle getExtras() { 1728 return mExtras; 1729 } 1730 } 1731} 1732