1 /* 2 * Copyright 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.media; 18 19 import static android.support.v4.media.MediaBrowserCompat.EXTRA_PAGE; 20 import static android.support.v4.media.MediaBrowserCompat.EXTRA_PAGE_SIZE; 21 22 import static androidx.media.MediaConstants2.ARGUMENT_EXTRAS; 23 import static androidx.media.MediaConstants2.ARGUMENT_PAGE; 24 import static androidx.media.MediaConstants2.ARGUMENT_PAGE_SIZE; 25 26 import android.app.PendingIntent; 27 import android.content.Intent; 28 import android.os.BadParcelableException; 29 import android.os.Bundle; 30 import android.os.IBinder; 31 import android.support.v4.media.MediaBrowserCompat.MediaItem; 32 33 import androidx.annotation.NonNull; 34 import androidx.annotation.Nullable; 35 import androidx.media.MediaLibraryService2.MediaLibrarySession.Builder; 36 import androidx.media.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback; 37 import androidx.media.MediaSession2.ControllerInfo; 38 39 import java.util.List; 40 import java.util.concurrent.Executor; 41 42 /** 43 * Base class for media library services. 44 * <p> 45 * Media library services enable applications to browse media content provided by an application 46 * and ask the application to start playing it. They may also be used to control content that 47 * is already playing by way of a {@link MediaSession2}. 48 * <p> 49 * When extending this class, also add the following to your {@code AndroidManifest.xml}. 50 * <pre> 51 * <service android:name="component_name_of_your_implementation" > 52 * <intent-filter> 53 * <action android:name="android.media.MediaLibraryService2" /> 54 * </intent-filter> 55 * </service></pre> 56 * <p> 57 * The {@link MediaLibraryService2} class derives from {@link MediaSessionService2}. IDs shouldn't 58 * be shared between the {@link MediaSessionService2} and {@link MediaSession2}. By 59 * default, an empty string will be used for ID of the service. If you want to specify an ID, 60 * declare metadata in the manifest as follows. 61 * 62 * @see MediaSessionService2 63 */ 64 public abstract class MediaLibraryService2 extends MediaSessionService2 { 65 /** 66 * This is the interface name that a service implementing a session service should say that it 67 * support -- that is, this is the action it uses for its intent filter. 68 */ 69 public static final String SERVICE_INTERFACE = "android.media.MediaLibraryService2"; 70 71 /** 72 * Session for the {@link MediaLibraryService2}. Build this object with 73 * {@link Builder} and return in {@link #onCreateSession(String)}. 74 */ 75 public static final class MediaLibrarySession extends MediaSession2 { 76 /** 77 * Callback for the {@link MediaLibrarySession}. 78 */ 79 public static class MediaLibrarySessionCallback extends MediaSession2.SessionCallback { 80 /** 81 * Called to get the root information for browsing by a particular client. 82 * <p> 83 * The implementation should verify that the client package has permission 84 * to access browse media information before returning the root id; it 85 * should return null if the client is not allowed to access this 86 * information. 87 * <p> 88 * Note: this callback may be called on the main thread, regardless of the callback 89 * executor. 90 * 91 * @param session the session for this event 92 * @param controllerInfo information of the controller requesting access to browse 93 * media. 94 * @param extras An optional bundle of service-specific arguments to send 95 * to the media library service when connecting and retrieving the 96 * root id for browsing, or null if none. The contents of this 97 * bundle may affect the information returned when browsing. 98 * @return The {@link LibraryRoot} for accessing this app's content or null. 99 * @see LibraryRoot#EXTRA_RECENT 100 * @see LibraryRoot#EXTRA_OFFLINE 101 * @see LibraryRoot#EXTRA_SUGGESTED 102 * @see SessionCommand2#COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT 103 */ 104 public @Nullable LibraryRoot onGetLibraryRoot(@NonNull MediaLibrarySession session, 105 @NonNull ControllerInfo controllerInfo, @Nullable Bundle extras) { 106 return null; 107 } 108 109 /** 110 * Called to get an item. Return result here for the browser. 111 * <p> 112 * Return {@code null} for no result or error. 113 * 114 * @param session the session for this event 115 * @param mediaId item id to get media item. 116 * @return a media item. {@code null} for no result or error. 117 * @see SessionCommand2#COMMAND_CODE_LIBRARY_GET_ITEM 118 */ 119 public @Nullable MediaItem2 onGetItem(@NonNull MediaLibrarySession session, 120 @NonNull ControllerInfo controllerInfo, @NonNull String mediaId) { 121 return null; 122 } 123 124 /** 125 * Called to get children of given parent id. Return the children here for the browser. 126 * <p> 127 * Return an empty list for no children, and return {@code null} for the error. 128 * 129 * @param session the session for this event 130 * @param parentId parent id to get children 131 * @param page number of page 132 * @param pageSize size of the page 133 * @param extras extra bundle 134 * @return list of children. Can be {@code null}. 135 * @see SessionCommand2#COMMAND_CODE_LIBRARY_GET_CHILDREN 136 */ 137 public @Nullable List<MediaItem2> onGetChildren(@NonNull MediaLibrarySession session, 138 @NonNull ControllerInfo controller, @NonNull String parentId, int page, 139 int pageSize, @Nullable Bundle extras) { 140 return null; 141 } 142 143 /** 144 * Called when a controller subscribes to the parent. 145 * <p> 146 * It's your responsibility to keep subscriptions by your own and call 147 * {@link MediaLibrarySession#notifyChildrenChanged(ControllerInfo, String, int, Bundle)} 148 * when the parent is changed. 149 * 150 * @param session the session for this event 151 * @param controller controller 152 * @param parentId parent id 153 * @param extras extra bundle 154 * @see SessionCommand2#COMMAND_CODE_LIBRARY_SUBSCRIBE 155 */ 156 public void onSubscribe(@NonNull MediaLibrarySession session, 157 @NonNull ControllerInfo controller, @NonNull String parentId, 158 @Nullable Bundle extras) { 159 } 160 161 /** 162 * Called when a controller unsubscribes to the parent. 163 * 164 * @param session the session for this event 165 * @param controller controller 166 * @param parentId parent id 167 * @see SessionCommand2#COMMAND_CODE_LIBRARY_UNSUBSCRIBE 168 */ 169 public void onUnsubscribe(@NonNull MediaLibrarySession session, 170 @NonNull ControllerInfo controller, @NonNull String parentId) { 171 } 172 173 /** 174 * Called when a controller requests search. 175 * 176 * @param session the session for this event 177 * @param query The search query sent from the media browser. It contains keywords 178 * separated by space. 179 * @param extras The bundle of service-specific arguments sent from the media browser. 180 * @see SessionCommand2#COMMAND_CODE_LIBRARY_SEARCH 181 */ 182 public void onSearch(@NonNull MediaLibrarySession session, 183 @NonNull ControllerInfo controllerInfo, @NonNull String query, 184 @Nullable Bundle extras) { 185 } 186 187 /** 188 * Called to get the search result. Return search result here for the browser which has 189 * requested search previously. 190 * <p> 191 * Return an empty list for no search result, and return {@code null} for the error. 192 * 193 * @param session the session for this event 194 * @param controllerInfo Information of the controller requesting the search result. 195 * @param query The search query which was previously sent through 196 * {@link #onSearch(MediaLibrarySession, ControllerInfo, String, Bundle)}. 197 * @param page page number. Starts from {@code 1}. 198 * @param pageSize page size. Should be greater or equal to {@code 1}. 199 * @param extras The bundle of service-specific arguments sent from the media browser. 200 * @return search result. {@code null} for error. 201 * @see SessionCommand2#COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT 202 */ 203 public @Nullable List<MediaItem2> onGetSearchResult( 204 @NonNull MediaLibrarySession session, @NonNull ControllerInfo controllerInfo, 205 @NonNull String query, int page, int pageSize, @Nullable Bundle extras) { 206 return null; 207 } 208 } 209 210 /** 211 * Builder for {@link MediaLibrarySession}. 212 */ 213 // Override all methods just to show them with the type instead of generics in Javadoc. 214 // This workarounds javadoc issue described in the MediaSession2.BuilderBase. 215 // Note: Don't override #setSessionCallback() because the callback can be set by the 216 // constructor. 217 public static final class Builder extends MediaSession2.BuilderBase<MediaLibrarySession, 218 Builder, MediaLibrarySessionCallback> { 219 private MediaLibrarySessionImplBase.Builder mImpl; 220 221 // Builder requires MediaLibraryService2 instead of Context just to ensure that the 222 // builder can be only instantiated within the MediaLibraryService2. 223 // Ideally it's better to make it inner class of service to enforce, it violates API 224 // guideline that Builders should be the inner class of the building target. 225 public Builder(@NonNull MediaLibraryService2 service, 226 @NonNull Executor callbackExecutor, 227 @NonNull MediaLibrarySessionCallback callback) { 228 super(service); 229 mImpl = new MediaLibrarySessionImplBase.Builder(service); 230 setImpl(mImpl); 231 setSessionCallback(callbackExecutor, callback); 232 } 233 234 @Override 235 public @NonNull Builder setPlayer(@NonNull MediaPlayerInterface player) { 236 return super.setPlayer(player); 237 } 238 239 @Override 240 public @NonNull Builder setPlaylistAgent(@NonNull MediaPlaylistAgent playlistAgent) { 241 return super.setPlaylistAgent(playlistAgent); 242 } 243 244 @Override 245 public @NonNull Builder setVolumeProvider( 246 @Nullable VolumeProviderCompat volumeProvider) { 247 return super.setVolumeProvider(volumeProvider); 248 } 249 250 @Override 251 public @NonNull Builder setSessionActivity(@Nullable PendingIntent pi) { 252 return super.setSessionActivity(pi); 253 } 254 255 @Override 256 public @NonNull Builder setId(@NonNull String id) { 257 return super.setId(id); 258 } 259 260 @Override 261 public @NonNull MediaLibrarySession build() { 262 return super.build(); 263 } 264 } 265 266 MediaLibrarySession(SupportLibraryImpl impl) { 267 super(impl); 268 } 269 270 /** 271 * Notify the controller of the change in a parent's children. 272 * <p> 273 * If the controller hasn't subscribed to the parent, the API will do nothing. 274 * <p> 275 * Controllers will use {@link MediaBrowser2#getChildren(String, int, int, Bundle)} to get 276 * the list of children. 277 * 278 * @param controller controller to notify 279 * @param parentId parent id with changes in its children 280 * @param itemCount number of children. 281 * @param extras extra information from session to controller 282 */ 283 public void notifyChildrenChanged(@NonNull ControllerInfo controller, 284 @NonNull String parentId, int itemCount, @Nullable Bundle extras) { 285 List<MediaSessionManager.RemoteUserInfo> subscribingBrowsers = 286 getServiceCompat().getSubscribingBrowsers(parentId); 287 getImpl().notifyChildrenChanged(controller, parentId, itemCount, extras, 288 subscribingBrowsers); 289 } 290 291 /** 292 * Notify all controllers that subscribed to the parent about change in the parent's 293 * children, regardless of the extra bundle supplied by 294 * {@link MediaBrowser2#subscribe(String, Bundle)}. 295 * 296 * @param parentId parent id 297 * @param itemCount number of children 298 * @param extras extra information from session to controller 299 */ 300 // This is for the backward compatibility. 301 public void notifyChildrenChanged(@NonNull String parentId, int itemCount, 302 @Nullable Bundle extras) { 303 if (extras == null) { 304 getServiceCompat().notifyChildrenChanged(parentId); 305 } else { 306 getServiceCompat().notifyChildrenChanged(parentId, extras); 307 } 308 } 309 310 /** 311 * Notify controller about change in the search result. 312 * 313 * @param controller controller to notify 314 * @param query previously sent search query from the controller. 315 * @param itemCount the number of items that have been found in the search. 316 * @param extras extra bundle 317 */ 318 public void notifySearchResultChanged(@NonNull ControllerInfo controller, 319 @NonNull String query, int itemCount, @Nullable Bundle extras) { 320 getImpl().notifySearchResultChanged(controller, query, itemCount, extras); 321 } 322 323 private MediaLibraryService2 getService() { 324 return (MediaLibraryService2) getContext(); 325 } 326 327 private MediaBrowserServiceCompat getServiceCompat() { 328 return getService().getServiceCompat(); 329 } 330 331 @Override 332 MediaLibrarySessionCallback getCallback() { 333 return (MediaLibrarySessionCallback) super.getCallback(); 334 } 335 } 336 337 @Override 338 MediaBrowserServiceCompat createBrowserServiceCompat() { 339 return new MyBrowserService(); 340 } 341 342 @Override 343 int getSessionType() { 344 return SessionToken2.TYPE_LIBRARY_SERVICE; 345 } 346 347 @Override 348 public void onCreate() { 349 super.onCreate(); 350 351 MediaSession2 session = getSession(); 352 if (!(session instanceof MediaLibrarySession)) { 353 throw new RuntimeException("Expected MediaLibrarySession, but returned MediaSession2"); 354 } 355 } 356 357 private MediaLibrarySession getLibrarySession() { 358 return (MediaLibrarySession) getSession(); 359 } 360 361 @Override 362 public IBinder onBind(Intent intent) { 363 return super.onBind(intent); 364 } 365 366 /** 367 * Called when another app requested to start this service. 368 * <p> 369 * Library service will accept or reject the connection with the 370 * {@link MediaLibrarySessionCallback} in the created session. 371 * <p> 372 * Service wouldn't run if {@code null} is returned or session's ID doesn't match with the 373 * expected ID that you've specified through the AndroidManifest.xml. 374 * <p> 375 * This method will be called on the main thread. 376 * 377 * @param sessionId session id written in the AndroidManifest.xml. 378 * @return a new library session 379 * @see Builder 380 * @see #getSession() 381 * @throws RuntimeException if returned session is invalid 382 */ 383 @Override 384 public @NonNull abstract MediaLibrarySession onCreateSession(String sessionId); 385 386 /** 387 * Contains information that the library service needs to send to the client when 388 * {@link MediaBrowser2#getLibraryRoot(Bundle)} is called. 389 */ 390 public static final class LibraryRoot { 391 /** 392 * The lookup key for a boolean that indicates whether the library service should return a 393 * librar root for recently played media items. 394 * 395 * <p>When creating a media browser for a given media library service, this key can be 396 * supplied as a root hint for retrieving media items that are recently played. 397 * If the media library service can provide such media items, the implementation must return 398 * the key in the root hint when 399 * {@link MediaLibrarySessionCallback#onGetLibraryRoot} 400 * is called back. 401 * 402 * <p>The root hint may contain multiple keys. 403 * 404 * @see #EXTRA_OFFLINE 405 * @see #EXTRA_SUGGESTED 406 */ 407 public static final String EXTRA_RECENT = "android.media.extra.RECENT"; 408 409 /** 410 * The lookup key for a boolean that indicates whether the library service should return a 411 * library root for offline media items. 412 * 413 * <p>When creating a media browser for a given media library service, this key can be 414 * supplied as a root hint for retrieving media items that are can be played without an 415 * internet connection. 416 * If the media library service can provide such media items, the implementation must return 417 * the key in the root hint when 418 * {@link MediaLibrarySessionCallback#onGetLibraryRoot} 419 * is called back. 420 * 421 * <p>The root hint may contain multiple keys. 422 * 423 * @see #EXTRA_RECENT 424 * @see #EXTRA_SUGGESTED 425 */ 426 public static final String EXTRA_OFFLINE = "android.media.extra.OFFLINE"; 427 428 /** 429 * The lookup key for a boolean that indicates whether the library service should return a 430 * library root for suggested media items. 431 * 432 * <p>When creating a media browser for a given media library service, this key can be 433 * supplied as a root hint for retrieving the media items suggested by the media library 434 * service. The list of media items is considered ordered by relevance, first being the top 435 * suggestion. 436 * If the media library service can provide such media items, the implementation must return 437 * the key in the root hint when 438 * {@link MediaLibrarySessionCallback#onGetLibraryRoot} 439 * is called back. 440 * 441 * <p>The root hint may contain multiple keys. 442 * 443 * @see #EXTRA_RECENT 444 * @see #EXTRA_OFFLINE 445 */ 446 public static final String EXTRA_SUGGESTED = "android.media.extra.SUGGESTED"; 447 448 private final String mRootId; 449 private final Bundle mExtras; 450 451 //private final LibraryRootProvider mProvider; 452 453 /** 454 * Constructs a library root. 455 * @param rootId The root id for browsing. 456 * @param extras Any extras about the library service. 457 */ 458 public LibraryRoot(@NonNull String rootId, @Nullable Bundle extras) { 459 if (rootId == null) { 460 throw new IllegalArgumentException("rootId shouldn't be null"); 461 } 462 mRootId = rootId; 463 mExtras = extras; 464 } 465 466 /** 467 * Gets the root id for browsing. 468 */ 469 public String getRootId() { 470 return mRootId; 471 } 472 473 /** 474 * Gets any extras about the library service. 475 */ 476 public Bundle getExtras() { 477 return mExtras; 478 } 479 } 480 481 private class MyBrowserService extends MediaBrowserServiceCompat { 482 @Override 483 public BrowserRoot onGetRoot(String clientPackageName, int clientUid, 484 final Bundle extras) { 485 if (MediaUtils2.isDefaultLibraryRootHint(extras)) { 486 // For connection request from the MediaController2. accept the connection from 487 // here, and let MediaLibrarySession decide whether to accept or reject the 488 // controller. 489 return sDefaultBrowserRoot; 490 } 491 final ControllerInfo controller = getController(); 492 MediaLibrarySession session = getLibrarySession(); 493 // Call onGetLibraryRoot() directly instead of execute on the executor. Here's the 494 // reason. 495 // We need to return browser root here. So if we run the callback on the executor, we 496 // should wait for the completion. 497 // However, we cannot wait if the callback executor is the main executor, which posts 498 // the runnable to the main thread's. In that case, since this onGetRoot() always runs 499 // on the main thread, the posted runnable for calling onGetLibraryRoot() wouldn't run 500 // in here. Even worse, we cannot know whether it would be run on the main thread or 501 // not. 502 // Because of the reason, just call onGetLibraryRoot directly here. onGetLibraryRoot() 503 // has documentation that it may be called on the main thread. 504 LibraryRoot libraryRoot = session.getCallback().onGetLibraryRoot( 505 session, controller, extras); 506 if (libraryRoot == null) { 507 return null; 508 } 509 return new BrowserRoot(libraryRoot.getRootId(), libraryRoot.getExtras()); 510 } 511 512 @Override 513 public void onLoadChildren(String parentId, Result<List<MediaItem>> result) { 514 onLoadChildren(parentId, result, null); 515 } 516 517 @Override 518 public void onLoadChildren(final String parentId, final Result<List<MediaItem>> result, 519 final Bundle options) { 520 result.detach(); 521 final ControllerInfo controller = getController(); 522 getLibrarySession().getCallbackExecutor().execute(new Runnable() { 523 @Override 524 public void run() { 525 if (options != null) { 526 options.setClassLoader(MediaLibraryService2.this.getClassLoader()); 527 try { 528 int page = options.getInt(EXTRA_PAGE); 529 int pageSize = options.getInt(EXTRA_PAGE_SIZE); 530 if (page > 0 && pageSize > 0) { 531 // Requesting the list of children through pagination. 532 List<MediaItem2> children = getLibrarySession().getCallback() 533 .onGetChildren(getLibrarySession(), controller, parentId, 534 page, pageSize, options); 535 result.sendResult(MediaUtils2.fromMediaItem2List(children)); 536 return; 537 } else if (options.containsKey( 538 MediaBrowser2.MEDIA_BROWSER2_SUBSCRIBE)) { 539 // This onLoadChildren() was triggered by MediaBrowser2.subscribe(). 540 options.remove(MediaBrowser2.MEDIA_BROWSER2_SUBSCRIBE); 541 getLibrarySession().getCallback().onSubscribe(getLibrarySession(), 542 controller, parentId, options.getBundle(ARGUMENT_EXTRAS)); 543 return; 544 } 545 } catch (BadParcelableException e) { 546 // pass-through. 547 } 548 } 549 List<MediaItem2> children = getLibrarySession().getCallback() 550 .onGetChildren(getLibrarySession(), controller, parentId, 551 1 /* page */, Integer.MAX_VALUE /* pageSize*/, 552 null /* extras */); 553 result.sendResult(MediaUtils2.fromMediaItem2List(children)); 554 } 555 }); 556 } 557 558 @Override 559 public void onLoadItem(final String itemId, final Result<MediaItem> result) { 560 result.detach(); 561 final ControllerInfo controller = getController(); 562 getLibrarySession().getCallbackExecutor().execute(new Runnable() { 563 @Override 564 public void run() { 565 MediaItem2 item = getLibrarySession().getCallback().onGetItem( 566 getLibrarySession(), controller, itemId); 567 if (item == null) { 568 result.sendResult(null); 569 } else { 570 result.sendResult(MediaUtils2.createMediaItem(item)); 571 } 572 } 573 }); 574 } 575 576 @Override 577 public void onSearch(final String query, final Bundle extras, 578 final Result<List<MediaItem>> result) { 579 result.detach(); 580 final ControllerInfo controller = getController(); 581 extras.setClassLoader(MediaLibraryService2.this.getClassLoader()); 582 try { 583 final int page = extras.getInt(ARGUMENT_PAGE); 584 final int pageSize = extras.getInt(ARGUMENT_PAGE_SIZE); 585 if (!(page > 0 && pageSize > 0)) { 586 getLibrarySession().getCallbackExecutor().execute(new Runnable() { 587 @Override 588 public void run() { 589 getLibrarySession().getCallback().onSearch( 590 getLibrarySession(), controller, query, extras); 591 } 592 }); 593 } else { 594 getLibrarySession().getCallbackExecutor().execute(new Runnable() { 595 @Override 596 public void run() { 597 List<MediaItem2> searchResult = getLibrarySession().getCallback() 598 .onGetSearchResult(getLibrarySession(), controller, query, 599 page, pageSize, extras); 600 if (searchResult == null) { 601 result.sendResult(null); 602 return; 603 } 604 result.sendResult(MediaUtils2.fromMediaItem2List(searchResult)); 605 } 606 }); 607 } 608 } catch (BadParcelableException e) { 609 // Do nothing. 610 } 611 } 612 613 @Override 614 public void onCustomAction(String action, Bundle extras, Result<Bundle> result) { 615 // No-op. Library session will handle the custom action. 616 } 617 618 private ControllerInfo getController() { 619 MediaLibrarySession session = getLibrarySession(); 620 List<ControllerInfo> controllers = session.getConnectedControllers(); 621 622 MediaSessionManager.RemoteUserInfo info = getCurrentBrowserInfo(); 623 if (info == null) { 624 return null; 625 } 626 627 for (int i = 0; i < controllers.size(); i++) { 628 // Note: This cannot pick the right controller between two controllers in same 629 // process. 630 ControllerInfo controller = controllers.get(i); 631 if (controller.getPackageName().equals(info.getPackageName()) 632 && controller.getUid() == info.getUid()) { 633 return controller; 634 } 635 } 636 return null; 637 } 638 } 639 } 640