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