Java tutorial
/* * 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); } }); } } }