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