blob: 6ef7fcf7dc88ac267cba5e954977f4681d95c6b9 [file] [log] [blame]
/*
* 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);
}
});
}
}
}