| /* |
| * Copyright 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.media; |
| |
| import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; |
| |
| import android.content.Context; |
| import android.os.Bundle; |
| import android.support.v4.media.MediaBrowserCompat; |
| import android.support.v4.media.MediaBrowserCompat.ItemCallback; |
| import android.support.v4.media.MediaBrowserCompat.MediaItem; |
| import android.support.v4.media.MediaBrowserCompat.SubscriptionCallback; |
| |
| import androidx.annotation.GuardedBy; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.RestrictTo; |
| import androidx.media.MediaLibraryService2.MediaLibrarySession; |
| import androidx.media.MediaSession2.ControllerInfo; |
| |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.concurrent.Executor; |
| |
| /** |
| * @hide |
| * Browses media content offered by a {@link MediaLibraryService2}. |
| */ |
| @RestrictTo(LIBRARY_GROUP) |
| public class MediaBrowser2 extends MediaController2 { |
| /** |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP) |
| public static final String EXTRA_ITEM_COUNT = "android.media.browse.extra.ITEM_COUNT"; |
| |
| /** |
| * Key for Bundle version of {@link MediaSession2.ControllerInfo}. |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP) |
| public static final String EXTRA_TARGET = "android.media.browse.extra.TARGET"; |
| |
| private final Object mLock = new Object(); |
| @GuardedBy("mLock") |
| private final HashMap<Bundle, MediaBrowserCompat> mBrowserCompats = new HashMap<>(); |
| @GuardedBy("mLock") |
| private final HashMap<String, List<SubscribeCallback>> mSubscribeCallbacks = new HashMap<>(); |
| |
| /** |
| * Callback to listen events from {@link MediaLibraryService2}. |
| */ |
| public static class BrowserCallback extends MediaController2.ControllerCallback { |
| /** |
| * Called with the result of {@link #getLibraryRoot(Bundle)}. |
| * <p> |
| * {@code rootMediaId} and {@code rootExtra} can be {@code null} if the library root isn't |
| * available. |
| * |
| * @param browser the browser for this event |
| * @param rootHints rootHints that you previously requested. |
| * @param rootMediaId media id of the library root. Can be {@code null} |
| * @param rootExtra extra of the library root. Can be {@code null} |
| */ |
| public void onGetLibraryRootDone(@NonNull MediaBrowser2 browser, @Nullable Bundle rootHints, |
| @Nullable String rootMediaId, @Nullable Bundle rootExtra) { } |
| |
| /** |
| * Called when there's change in the parent's children. |
| * <p> |
| * This API is called when the library service called |
| * {@link MediaLibrarySession#notifyChildrenChanged(ControllerInfo, String, int, Bundle)} or |
| * {@link MediaLibrarySession#notifyChildrenChanged(String, int, Bundle)} for the parent. |
| * |
| * @param browser the browser for this event |
| * @param parentId parent id that you've specified with {@link #subscribe(String, Bundle)} |
| * @param itemCount number of children |
| * @param extras extra bundle from the library service. Can be differ from extras that |
| * you've specified with {@link #subscribe(String, Bundle)}. |
| */ |
| public void onChildrenChanged(@NonNull MediaBrowser2 browser, @NonNull String parentId, |
| int itemCount, @Nullable Bundle extras) { } |
| |
| /** |
| * Called when the list of items has been returned by the library service for the previous |
| * {@link MediaBrowser2#getChildren(String, int, int, Bundle)}. |
| * |
| * @param browser the browser for this event |
| * @param parentId parent id |
| * @param page page number that you've specified with |
| * {@link #getChildren(String, int, int, Bundle)} |
| * @param pageSize page size that you've specified with |
| * {@link #getChildren(String, int, int, Bundle)} |
| * @param result result. Can be {@code null} |
| * @param extras extra bundle from the library service |
| */ |
| public void onGetChildrenDone(@NonNull MediaBrowser2 browser, @NonNull String parentId, |
| int page, int pageSize, @Nullable List<MediaItem2> result, |
| @Nullable Bundle extras) { } |
| |
| /** |
| * Called when the item has been returned by the library service for the previous |
| * {@link MediaBrowser2#getItem(String)} call. |
| * <p> |
| * Result can be null if there had been error. |
| * |
| * @param browser the browser for this event |
| * @param mediaId media id |
| * @param result result. Can be {@code null} |
| */ |
| public void onGetItemDone(@NonNull MediaBrowser2 browser, @NonNull String mediaId, |
| @Nullable MediaItem2 result) { } |
| |
| /** |
| * Called when there's change in the search result requested by the previous |
| * {@link MediaBrowser2#search(String, Bundle)}. |
| * |
| * @param browser the browser for this event |
| * @param query search query that you've specified with {@link #search(String, Bundle)} |
| * @param itemCount The item count for the search result |
| * @param extras extra bundle from the library service |
| */ |
| public void onSearchResultChanged(@NonNull MediaBrowser2 browser, @NonNull String query, |
| int itemCount, @Nullable Bundle extras) { } |
| |
| /** |
| * Called when the search result has been returned by the library service for the previous |
| * {@link MediaBrowser2#getSearchResult(String, int, int, Bundle)}. |
| * <p> |
| * Result can be null if there had been error. |
| * |
| * @param browser the browser for this event |
| * @param query search query that you've specified with |
| * {@link #getSearchResult(String, int, int, Bundle)} |
| * @param page page number that you've specified with |
| * {@link #getSearchResult(String, int, int, Bundle)} |
| * @param pageSize page size that you've specified with |
| * {@link #getSearchResult(String, int, int, Bundle)} |
| * @param result result. Can be {@code null}. |
| * @param extras extra bundle from the library service |
| */ |
| public void onGetSearchResultDone(@NonNull MediaBrowser2 browser, @NonNull String query, |
| int page, int pageSize, @Nullable List<MediaItem2> result, |
| @Nullable Bundle extras) { } |
| } |
| |
| public MediaBrowser2(@NonNull Context context, @NonNull SessionToken2 token, |
| @NonNull /*@CallbackExecutor*/ Executor executor, @NonNull BrowserCallback callback) { |
| super(context, token, executor, callback); |
| } |
| |
| @Override |
| public void close() { |
| synchronized (mLock) { |
| for (MediaBrowserCompat browser : mBrowserCompats.values()) { |
| browser.disconnect(); |
| } |
| mBrowserCompats.clear(); |
| // TODO: Ensure that ControllerCallback#onDisconnected() is called by super.close(). |
| super.close(); |
| } |
| } |
| |
| /** |
| * Get the library root. Result would be sent back asynchronously with the |
| * {@link BrowserCallback#onGetLibraryRootDone(MediaBrowser2, Bundle, String, Bundle)}. |
| * |
| * @param extras extras for getting root |
| * @see BrowserCallback#onGetLibraryRootDone(MediaBrowser2, Bundle, String, Bundle) |
| */ |
| public void getLibraryRoot(@Nullable final Bundle extras) { |
| final MediaBrowserCompat browser = getBrowserCompat(extras); |
| if (browser != null) { |
| // Already connected with the given extras. |
| getCallbackExecutor().execute(new Runnable() { |
| @Override |
| public void run() { |
| getCallback().onGetLibraryRootDone(MediaBrowser2.this, extras, |
| browser.getRoot(), browser.getExtras()); |
| } |
| }); |
| } else { |
| MediaBrowserCompat newBrowser = new MediaBrowserCompat(getContext(), |
| getSessionToken().getComponentName(), new GetLibraryRootCallback(extras), |
| extras); |
| newBrowser.connect(); |
| synchronized (mLock) { |
| mBrowserCompats.put(extras, newBrowser); |
| } |
| } |
| } |
| |
| /** |
| * Subscribe to a parent id for the change in its children. When there's a change, |
| * {@link BrowserCallback#onChildrenChanged(MediaBrowser2, String, int, Bundle)} will be called |
| * with the bundle that you've specified. You should call |
| * {@link #getChildren(String, int, int, Bundle)} to get the actual contents for the parent. |
| * |
| * @param parentId parent id |
| * @param extras extra bundle |
| */ |
| public void subscribe(@NonNull String parentId, @Nullable Bundle extras) { |
| if (parentId == null) { |
| throw new IllegalArgumentException("parentId shouldn't be null"); |
| } |
| // TODO: Document this behavior |
| Bundle option; |
| if (extras != null && (extras.containsKey(MediaBrowserCompat.EXTRA_PAGE) |
| || extras.containsKey(MediaBrowserCompat.EXTRA_PAGE_SIZE))) { |
| option = new Bundle(extras); |
| option.remove(MediaBrowserCompat.EXTRA_PAGE); |
| option.remove(MediaBrowserCompat.EXTRA_PAGE_SIZE); |
| } else { |
| option = extras; |
| } |
| SubscribeCallback callback = new SubscribeCallback(); |
| synchronized (mLock) { |
| List<SubscribeCallback> list = mSubscribeCallbacks.get(parentId); |
| if (list == null) { |
| list = new ArrayList<>(); |
| mSubscribeCallbacks.put(parentId, list); |
| } |
| list.add(callback); |
| } |
| // TODO: Revisit using default browser is OK. Here's my concern. |
| // Assume that MediaBrowser2 is connected with the MediaBrowserServiceCompat. |
| // Since MediaBrowserServiceCompat can call MediaBrowserServiceCompat# |
| // getBrowserRootHints(), the service may refuse calls from MediaBrowser2 |
| getBrowserCompat().subscribe(parentId, option, callback); |
| } |
| |
| /** |
| * Unsubscribe for changes to the children of the parent, which was previously subscribed with |
| * {@link #subscribe(String, Bundle)}. |
| * <p> |
| * This unsubscribes all previous subscription with the parent id, regardless of the extra |
| * that was previously sent to the library service. |
| * |
| * @param parentId parent id |
| */ |
| public void unsubscribe(@NonNull String parentId) { |
| if (parentId == null) { |
| throw new IllegalArgumentException("parentId shouldn't be null"); |
| } |
| // Note: don't use MediaBrowserCompat#unsubscribe(String) here, to keep the subscription |
| // callback for getChildren. |
| synchronized (mLock) { |
| List<SubscribeCallback> list = mSubscribeCallbacks.get(parentId); |
| if (list == null) { |
| return; |
| } |
| MediaBrowserCompat browser = getBrowserCompat(); |
| for (int i = 0; i < list.size(); i++) { |
| browser.unsubscribe(parentId, list.get(i)); |
| } |
| } |
| } |
| |
| /** |
| * Get list of children under the parent. Result would be sent back asynchronously with the |
| * {@link BrowserCallback#onGetChildrenDone(MediaBrowser2, String, int, int, List, Bundle)}. |
| * |
| * @param parentId parent id for getting the children. |
| * @param page page number to get the result. Starts from {@code 1} |
| * @param pageSize page size. Should be greater or equal to {@code 1} |
| * @param extras extra bundle |
| */ |
| public void getChildren(@NonNull String parentId, int page, int pageSize, |
| @Nullable Bundle extras) { |
| if (parentId == null) { |
| throw new IllegalArgumentException("parentId shouldn't be null"); |
| } |
| if (page < 1 || pageSize < 1) { |
| throw new IllegalArgumentException("Neither page nor pageSize should be less than 1"); |
| } |
| Bundle options = new Bundle(extras); |
| options.putInt(MediaBrowserCompat.EXTRA_PAGE, page); |
| options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize); |
| // TODO: Revisit using default browser is OK. See TODO in subscribe |
| getBrowserCompat().subscribe(parentId, options, |
| new GetChildrenCallback(parentId, page, pageSize)); |
| } |
| |
| /** |
| * Get the media item with the given media id. Result would be sent back asynchronously with the |
| * {@link BrowserCallback#onGetItemDone(MediaBrowser2, String, MediaItem2)}. |
| * |
| * @param mediaId media id for specifying the item |
| */ |
| public void getItem(@NonNull final String mediaId) { |
| // TODO: Revisit using default browser is OK. See TODO in subscribe |
| getBrowserCompat().getItem(mediaId, new ItemCallback() { |
| @Override |
| public void onItemLoaded(final MediaItem item) { |
| getCallbackExecutor().execute(new Runnable() { |
| @Override |
| public void run() { |
| getCallback().onGetItemDone(MediaBrowser2.this, mediaId, |
| MediaUtils2.createMediaItem2(item)); |
| } |
| }); |
| } |
| |
| @Override |
| public void onError(String itemId) { |
| getCallbackExecutor().execute(new Runnable() { |
| @Override |
| public void run() { |
| getCallback().onGetItemDone(MediaBrowser2.this, mediaId, null); |
| } |
| }); |
| } |
| }); |
| } |
| |
| /** |
| * Send a search request to the library service. When the search result is changed, |
| * {@link BrowserCallback#onSearchResultChanged(MediaBrowser2, String, int, Bundle)} will be |
| * called. You should call {@link #getSearchResult(String, int, int, Bundle)} to get the actual |
| * search result. |
| * |
| * @param query search query. Should not be an empty string. |
| * @param extras extra bundle |
| */ |
| public void search(@NonNull String query, @Nullable Bundle extras) { |
| // TODO: Implement |
| } |
| |
| /** |
| * Get the search result from lhe library service. Result would be sent back asynchronously with |
| * the |
| * {@link BrowserCallback#onGetSearchResultDone(MediaBrowser2, String, int, int, List, Bundle)}. |
| * |
| * @param query search query that you've specified with {@link #search(String, Bundle)} |
| * @param page page number to get search result. Starts from {@code 1} |
| * @param pageSize page size. Should be greater or equal to {@code 1} |
| * @param extras extra bundle |
| */ |
| public void getSearchResult(@NonNull String query, int page, int pageSize, |
| @Nullable Bundle extras) { |
| // TODO: Implement |
| } |
| |
| @Override |
| BrowserCallback getCallback() { |
| return (BrowserCallback) super.getCallback(); |
| } |
| |
| private MediaBrowserCompat getBrowserCompat(Bundle extras) { |
| synchronized (mLock) { |
| return mBrowserCompats.get(extras); |
| } |
| } |
| |
| private class GetLibraryRootCallback extends MediaBrowserCompat.ConnectionCallback { |
| private final Bundle mExtras; |
| |
| GetLibraryRootCallback(Bundle extras) { |
| super(); |
| mExtras = extras; |
| } |
| |
| @Override |
| public void onConnected() { |
| getCallbackExecutor().execute(new Runnable() { |
| @Override |
| public void run() { |
| MediaBrowserCompat browser; |
| synchronized (mLock) { |
| browser = mBrowserCompats.get(mExtras); |
| } |
| if (browser == null) { |
| // Shouldn't be happen. |
| return; |
| } |
| getCallback().onGetLibraryRootDone(MediaBrowser2.this, |
| mExtras, browser.getRoot(), browser.getExtras()); |
| } |
| }); |
| } |
| |
| @Override |
| public void onConnectionSuspended() { |
| close(); |
| } |
| |
| @Override |
| public void onConnectionFailed() { |
| close(); |
| } |
| } |
| |
| private class SubscribeCallback extends SubscriptionCallback { |
| @Override |
| public void onError(String parentId) { |
| onChildrenLoaded(parentId, null, null); |
| } |
| |
| @Override |
| public void onError(String parentId, Bundle options) { |
| onChildrenLoaded(parentId, null, options); |
| } |
| |
| @Override |
| public void onChildrenLoaded(String parentId, List<MediaItem> children) { |
| onChildrenLoaded(parentId, children, null); |
| } |
| |
| @Override |
| public void onChildrenLoaded(final String parentId, List<MediaItem> children, |
| final Bundle options) { |
| final int itemCount; |
| if (options != null && options.containsKey(EXTRA_ITEM_COUNT)) { |
| itemCount = options.getInt(EXTRA_ITEM_COUNT); |
| } else if (children != null) { |
| itemCount = children.size(); |
| } else { |
| // Currently no way to tell failures in MediaBrowser2#subscribe(). |
| return; |
| } |
| getCallbackExecutor().execute(new Runnable() { |
| @Override |
| public void run() { |
| getCallback().onChildrenChanged(MediaBrowser2.this, parentId, itemCount, |
| options); |
| } |
| }); |
| } |
| } |
| |
| private class GetChildrenCallback extends SubscriptionCallback { |
| private final String mParentId; |
| private final int mPage; |
| private final int mPageSize; |
| |
| GetChildrenCallback(String parentId, int page, int pageSize) { |
| super(); |
| mParentId = parentId; |
| mPage = page; |
| mPageSize = pageSize; |
| } |
| |
| @Override |
| public void onError(String parentId) { |
| onChildrenLoaded(parentId, null, null); |
| } |
| |
| @Override |
| public void onError(String parentId, Bundle options) { |
| onChildrenLoaded(parentId, null, options); |
| } |
| |
| @Override |
| public void onChildrenLoaded(String parentId, List<MediaItem> children) { |
| onChildrenLoaded(parentId, children, null); |
| } |
| |
| @Override |
| public void onChildrenLoaded(final String parentId, List<MediaItem> children, |
| final Bundle options) { |
| final List<MediaItem2> items; |
| if (children == null) { |
| items = null; |
| } else { |
| items = new ArrayList<>(); |
| for (int i = 0; i < children.size(); i++) { |
| items.add(MediaUtils2.createMediaItem2(children.get(i))); |
| } |
| } |
| getCallbackExecutor().execute(new Runnable() { |
| @Override |
| public void run() { |
| getCallback().onGetChildrenDone(MediaBrowser2.this, parentId, mPage, mPageSize, |
| items, options); |
| getBrowserCompat().unsubscribe(mParentId, GetChildrenCallback.this); |
| } |
| }); |
| } |
| } |
| } |