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