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 17 package androidx.media; 18 19 import static androidx.annotation.RestrictTo.Scope.LIBRARY; 20 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_ADD_SUBSCRIPTION; 21 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_CONNECT; 22 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_DISCONNECT; 23 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_GET_MEDIA_ITEM; 24 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_REGISTER_CALLBACK_MESSENGER; 25 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_REMOVE_SUBSCRIPTION; 26 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_SEARCH; 27 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_SEND_CUSTOM_ACTION; 28 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER; 29 import static androidx.media.MediaBrowserProtocol.DATA_CALLBACK_TOKEN; 30 import static androidx.media.MediaBrowserProtocol.DATA_CALLING_PID; 31 import static androidx.media.MediaBrowserProtocol.DATA_CALLING_UID; 32 import static androidx.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION; 33 import static androidx.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION_EXTRAS; 34 import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_ID; 35 import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_LIST; 36 import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_SESSION_TOKEN; 37 import static androidx.media.MediaBrowserProtocol.DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS; 38 import static androidx.media.MediaBrowserProtocol.DATA_OPTIONS; 39 import static androidx.media.MediaBrowserProtocol.DATA_PACKAGE_NAME; 40 import static androidx.media.MediaBrowserProtocol.DATA_RESULT_RECEIVER; 41 import static androidx.media.MediaBrowserProtocol.DATA_ROOT_HINTS; 42 import static androidx.media.MediaBrowserProtocol.DATA_SEARCH_EXTRAS; 43 import static androidx.media.MediaBrowserProtocol.DATA_SEARCH_QUERY; 44 import static androidx.media.MediaBrowserProtocol.EXTRA_CLIENT_VERSION; 45 import static androidx.media.MediaBrowserProtocol.EXTRA_MESSENGER_BINDER; 46 import static androidx.media.MediaBrowserProtocol.EXTRA_SERVICE_VERSION; 47 import static androidx.media.MediaBrowserProtocol.EXTRA_SESSION_BINDER; 48 import static androidx.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT; 49 import static androidx.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT_FAILED; 50 import static androidx.media.MediaBrowserProtocol.SERVICE_MSG_ON_LOAD_CHILDREN; 51 import static androidx.media.MediaBrowserProtocol.SERVICE_VERSION_CURRENT; 52 53 import android.app.Service; 54 import android.content.Context; 55 import android.content.Intent; 56 import android.content.pm.PackageManager; 57 import android.os.Binder; 58 import android.os.Build; 59 import android.os.Bundle; 60 import android.os.Handler; 61 import android.os.IBinder; 62 import android.os.Message; 63 import android.os.Messenger; 64 import android.os.Parcel; 65 import android.os.RemoteException; 66 import android.service.media.MediaBrowserService; 67 import android.support.v4.media.MediaBrowserCompat; 68 import android.support.v4.media.session.IMediaSession; 69 import android.support.v4.media.session.MediaSessionCompat; 70 import android.support.v4.os.ResultReceiver; 71 import android.text.TextUtils; 72 import android.util.Log; 73 74 import androidx.annotation.IntDef; 75 import androidx.annotation.NonNull; 76 import androidx.annotation.Nullable; 77 import androidx.annotation.RequiresApi; 78 import androidx.annotation.RestrictTo; 79 import androidx.collection.ArrayMap; 80 import androidx.core.app.BundleCompat; 81 import androidx.core.util.Pair; 82 import androidx.media.MediaSessionManager.RemoteUserInfo; 83 84 import java.io.FileDescriptor; 85 import java.io.PrintWriter; 86 import java.lang.annotation.Retention; 87 import java.lang.annotation.RetentionPolicy; 88 import java.util.ArrayList; 89 import java.util.Collections; 90 import java.util.HashMap; 91 import java.util.Iterator; 92 import 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 */ 121 public 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 { onCreate()185 void onCreate(); onBind(Intent intent)186 IBinder onBind(Intent intent); setSessionToken(MediaSessionCompat.Token token)187 void setSessionToken(MediaSessionCompat.Token token); notifyChildrenChanged(final String parentId, final Bundle options)188 void notifyChildrenChanged(final String parentId, final Bundle options); getBrowserRootHints()189 Bundle getBrowserRootHints(); getCurrentBrowserInfo()190 RemoteUserInfo getCurrentBrowserInfo(); getSubscribingBrowsers(String parentId)191 List<RemoteUserInfo> getSubscribingBrowsers(String parentId); 192 } 193 194 class MediaBrowserServiceImplBase implements MediaBrowserServiceImpl { 195 private Messenger mMessenger; 196 197 @Override onCreate()198 public void onCreate() { 199 mMessenger = new Messenger(mHandler); 200 } 201 202 @Override onBind(Intent intent)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 setSessionToken(final MediaSessionCompat.Token token)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 notifyChildrenChanged(@onNull final String parentId, final Bundle options)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 getBrowserRootHints()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 getCurrentBrowserInfo()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 getSubscribingBrowsers(String parentId)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 onCreate()294 public void onCreate() { 295 mServiceObj = MediaBrowserServiceCompatApi21.createService( 296 MediaBrowserServiceCompat.this, this); 297 MediaBrowserServiceCompatApi21.onCreate(mServiceObj); 298 } 299 300 @Override onBind(Intent intent)301 public IBinder onBind(Intent intent) { 302 return MediaBrowserServiceCompatApi21.onBind(mServiceObj, intent); 303 } 304 305 @Override setSessionToken(final MediaSessionCompat.Token token)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 notifyChildrenChanged(final String parentId, final Bundle options)326 public void notifyChildrenChanged(final String parentId, final Bundle options) { 327 notifyChildrenChangedForFramework(parentId, options); 328 notifyChildrenChangedForCompat(parentId, options); 329 } 330 331 @Override onGetRoot( String clientPackageName, int clientUid, Bundle rootHints)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 onLoadChildren(String parentId, final MediaBrowserServiceCompatApi21.ResultWrapper<List<Parcel>> resultWrapper)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 getSubscribingBrowsers(String parentId)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 notifyChildrenChangedForFramework(final String parentId, final Bundle options)409 void notifyChildrenChangedForFramework(final String parentId, final Bundle options) { 410 MediaBrowserServiceCompatApi21.notifyChildrenChanged(mServiceObj, parentId); 411 } 412 notifyChildrenChangedForCompat(final String parentId, final Bundle options)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 getBrowserRootHints()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 getCurrentBrowserInfo()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 onCreate()462 public void onCreate() { 463 mServiceObj = MediaBrowserServiceCompatApi23.createService( 464 MediaBrowserServiceCompat.this, this); 465 MediaBrowserServiceCompatApi21.onCreate(mServiceObj); 466 } 467 468 @Override onLoadItem(String itemId, final MediaBrowserServiceCompatApi21.ResultWrapper<Parcel> resultWrapper)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 onCreate()497 public void onCreate() { 498 mServiceObj = MediaBrowserServiceCompatApi26.createService( 499 MediaBrowserServiceCompat.this, this); 500 MediaBrowserServiceCompatApi21.onCreate(mServiceObj); 501 } 502 503 @Override onLoadChildren(String parentId, final MediaBrowserServiceCompatApi26.ResultWrapper resultWrapper, Bundle options)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 getBrowserRootHints()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 notifyChildrenChangedForFramework(final String parentId, final Bundle options)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 getCurrentBrowserInfo()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 ServiceHandler()569 ServiceHandler() { 570 } 571 572 @Override handleMessage(Message msg)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 sendMessageAtTime(Message msg, long uptimeMillis)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 postOrRun(Runnable r)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 ConnectionRecord(String pkg, int pid, int uid, Bundle rootHints, ServiceCallbacks callback)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 binderDied()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 Result(Object debug)710 Result(Object debug) { 711 mDebug = debug; 712 } 713 714 /** 715 * Send the result back to the caller. 716 */ sendResult(T result)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 */ sendProgressUpdate(Bundle extras)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 */ sendError(Bundle extras)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 */ detach()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 isDone()777 boolean isDone() { 778 return mDetachCalled || mSendResultCalled || mSendErrorCalled; 779 } 780 setFlags(@esultFlags int flags)781 void setFlags(@ResultFlags int flags) { 782 mFlags = flags; 783 } 784 getFlags()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 */ onResultSent(T result)793 void onResultSent(T result) { 794 } 795 796 /** 797 * Called when an interim update is sent. 798 */ onProgressUpdateSent(Bundle extras)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 */ onErrorSent(Bundle extras)808 void onErrorSent(Bundle extras) { 809 throw new UnsupportedOperationException("It is not supported to send an error for " 810 + mDebug); 811 } 812 checkExtraFields(Bundle extras)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 { ServiceBinderImpl()828 ServiceBinderImpl() { 829 } 830 connect(final String pkg, final int pid, final int uid, final Bundle rootHints, final ServiceCallbacks callbacks)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 disconnect(final ServiceCallbacks callbacks)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 addSubscription(final String id, final IBinder token, final Bundle options, final ServiceCallbacks callbacks)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 removeSubscription(final String id, final IBinder token, final ServiceCallbacks callbacks)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 getMediaItem(final String mediaId, final ResultReceiver receiver, final ServiceCallbacks callbacks)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. registerCallbacks(final ServiceCallbacks callbacks, final String pkg, final int pid, final int uid, final Bundle rootHints)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. unregisterCallbacks(final ServiceCallbacks callbacks)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 search(final String query, final Bundle extras, final ResultReceiver receiver, final ServiceCallbacks callbacks)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 sendCustomAction(final String action, final Bundle extras, final ResultReceiver receiver, final ServiceCallbacks callbacks)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 { asBinder()1041 IBinder asBinder(); onConnect(String root, MediaSessionCompat.Token session, Bundle extras)1042 void onConnect(String root, MediaSessionCompat.Token session, Bundle extras) 1043 throws RemoteException; onConnectFailed()1044 void onConnectFailed() throws RemoteException; onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, Bundle options, Bundle notifyChildrenChangedOptions)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 ServiceCallbacksCompat(Messenger callbacks)1052 ServiceCallbacksCompat(Messenger callbacks) { 1053 mCallbacks = callbacks; 1054 } 1055 1056 @Override asBinder()1057 public IBinder asBinder() { 1058 return mCallbacks.getBinder(); 1059 } 1060 1061 @Override onConnect(String root, MediaSessionCompat.Token session, Bundle extras)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 onConnectFailed()1076 public void onConnectFailed() throws RemoteException { 1077 sendRequest(SERVICE_MSG_ON_CONNECT_FAILED, null); 1078 } 1079 1080 @Override onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, Bundle options, Bundle notifyChildrenChangedOptions)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 sendRequest(int what, Bundle data)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) attachToBaseContext(Context base)1114 public void attachToBaseContext(Context base) { 1115 attachBaseContext(base); 1116 } 1117 1118 @Override onCreate()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 onBind(Intent intent)1136 public IBinder onBind(Intent intent) { 1137 return mImpl.onBind(intent); 1138 } 1139 1140 @Override dump(FileDescriptor fd, PrintWriter writer, String[] args)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 */ onGetRoot(@onNull String clientPackageName, int clientUid, @Nullable Bundle rootHints)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 */ onLoadChildren(@onNull String parentId, @NonNull Result<List<MediaBrowserCompat.MediaItem>> result)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 */ onLoadChildren(@onNull String parentId, @NonNull Result<List<MediaBrowserCompat.MediaItem>> result, @NonNull Bundle options)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 */ onLoadItem(String itemId, @NonNull Result<MediaBrowserCompat.MediaItem> result)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 */ onSearch(@onNull String query, Bundle extras, @NonNull Result<List<MediaBrowserCompat.MediaItem>> result)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 */ onCustomAction(@onNull String action, Bundle extras, @NonNull Result<Bundle> result)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 */ setSessionToken(MediaSessionCompat.Token token)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 */ getSessionToken()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 */ getBrowserRootHints()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 */ getCurrentBrowserInfo()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 */ notifyChildrenChanged(@onNull String parentId)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 */ notifyChildrenChanged(@onNull String parentId, @NonNull Bundle options)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) getSubscribingBrowsers(@onNull String parentId)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 */ isValidPackage(String pkg, int uid)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 */ addSubscription(String id, ConnectionRecord connection, IBinder token, Bundle options)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 */ removeSubscription(String id, ConnectionRecord connection, IBinder token)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 */ performLoadChildren(final String parentId, final ConnectionRecord connection, final Bundle subscribeOptions, final Bundle notifyChildrenChangedOptions)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 applyOptions(List<MediaBrowserCompat.MediaItem> list, final Bundle options)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 performLoadItem(String itemId, ConnectionRecord connection, final ResultReceiver receiver)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 performSearch(final String query, Bundle extras, ConnectionRecord connection, final ResultReceiver receiver)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 performCustomAction(final String action, Bundle extras, ConnectionRecord connection, final ResultReceiver receiver)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 */ BrowserRoot(@onNull String rootId, @Nullable Bundle extras)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 */ getRootId()1720 public String getRootId() { 1721 return mRootId; 1722 } 1723 1724 /** 1725 * Gets any extras about the browser service. 1726 */ getExtras()1727 public Bundle getExtras() { 1728 return mExtras; 1729 } 1730 } 1731 } 1732