summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Joe Onorato <joeo@google.com> 2014-07-22 23:21:12 -0700
committer Yao Chen <yaochen@google.com> 2014-07-23 17:56:57 -0700
commit69b078599b8d8bc3e8f94d6cab881145f4e2c129 (patch)
treea5e05fdfe4dd495b414a1e6f1f829ae1011f57f7
parent77bfc8f22892227ebc42afd282bd044742956be2 (diff)
Allow apps to be slow at loading children and thumbnails by making those functions allowed to be asynchronous.
Change-Id: Ibcaee3f0f8d9ba14f1b002df9c6d4594c6278045
-rw-r--r--api/current.txt9
-rw-r--r--media/java/android/media/browse/MediaBrowserService.java178
-rw-r--r--tests/MusicServiceDemo/src/com/example/android/musicservicedemo/BrowserService.java34
3 files changed, 181 insertions, 40 deletions
diff --git a/api/current.txt b/api/current.txt
index 164a3253b3a7..e57120c4ff96 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -16235,8 +16235,8 @@ package android.media.browse {
method public void notifyChildrenChanged(android.net.Uri);
method public android.os.IBinder onBind(android.content.Intent);
method public abstract android.media.browse.MediaBrowserService.BrowserRoot onGetRoot(java.lang.String, int, android.os.Bundle);
- method public abstract android.graphics.Bitmap onGetThumbnail(android.net.Uri, int, int);
- method public abstract java.util.List<android.media.browse.MediaBrowserItem> onLoadChildren(android.net.Uri);
+ method protected abstract void onLoadChildren(android.net.Uri, android.media.browse.MediaBrowserService.Result<java.util.List<android.media.browse.MediaBrowserItem>>);
+ method protected abstract void onLoadThumbnail(android.net.Uri, int, int, android.media.browse.MediaBrowserService.Result<android.graphics.Bitmap>);
method public void setSessionToken(android.media.session.MediaSession.Token);
field public static final java.lang.String SERVICE_ACTION = "android.media.browse.MediaBrowserService";
}
@@ -16247,6 +16247,11 @@ package android.media.browse {
method public android.net.Uri getRootUri();
}
+ public class MediaBrowserService.Result {
+ method public void detach();
+ method public void sendResult(T);
+ }
+
}
package android.media.effect {
diff --git a/media/java/android/media/browse/MediaBrowserService.java b/media/java/android/media/browse/MediaBrowserService.java
index 95a133f8877f..2d4bb7830aa6 100644
--- a/media/java/android/media/browse/MediaBrowserService.java
+++ b/media/java/android/media/browse/MediaBrowserService.java
@@ -70,6 +70,13 @@ import java.util.Set;
*/
public abstract class MediaBrowserService extends Service {
private static final String TAG = "MediaBrowserService";
+ private static final boolean DBG = false;
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ */
+ @SdkConstant(SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_ACTION = "android.media.browse.MediaBrowserService";
private final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap();
private final Handler mHandler = new Handler();
@@ -88,10 +95,65 @@ public abstract class MediaBrowserService extends Service {
}
/**
- * The {@link Intent} that must be declared as handled by the service.
+ * Completion handler for asynchronous callback methods in {@link MediaBrowserService}.
+ * <p>
+ * Each of the methods that takes one of these to send the result must call
+ * {@link #sendResult} to respond to the caller with the given results. If those
+ * functions return without calling {@link #sendResult}, they must instead call
+ * {@link #detach} before returning, and then may call {@link #sendResult} when
+ * they are done. If more than one of those methods is called, an exception will
+ * be thrown.
+ *
+ * @see MediaBrowserService#onLoadChildren
+ * @see MediaBrowserService#onLoadThumbnail
*/
- @SdkConstant(SdkConstantType.SERVICE_ACTION)
- public static final String SERVICE_ACTION = "android.media.browse.MediaBrowserService";
+ public class Result<T> {
+ private Object mDebug;
+ private boolean mDetachCalled;
+ private boolean mSendResultCalled;
+
+ Result(Object debug) {
+ mDebug = debug;
+ }
+
+ /**
+ * Send the result back to the caller.
+ */
+ public void sendResult(T result) {
+ if (mSendResultCalled) {
+ throw new IllegalStateException("sendResult() called twice for: " + mDebug);
+ }
+ mSendResultCalled = true;
+ onResultSent(result);
+ }
+
+ /**
+ * Detach this message from the current thread and allow the {@link #sendResult}
+ * call to happen later.
+ */
+ public void detach() {
+ if (mDetachCalled) {
+ throw new IllegalStateException("detach() called when detach() had already"
+ + " been called for: " + mDebug);
+ }
+ if (mSendResultCalled) {
+ throw new IllegalStateException("detach() called when sendResult() had already"
+ + " been called for: " + mDebug);
+ }
+ mDetachCalled = true;
+ }
+
+ boolean isDone() {
+ return mDetachCalled || mSendResultCalled;
+ }
+
+ /**
+ * Called when the result is sent, after assertions about not being called twice
+ * have happened.
+ */
+ void onResultSent(T result) {
+ }
+ }
private class ServiceBinder extends IMediaBrowserService.Stub {
@Override
@@ -206,14 +268,51 @@ public abstract class MediaBrowserService extends Service {
@Override
public void loadThumbnail(final Uri uri, final int width, final int height,
final IMediaBrowserServiceCallbacks callbacks) {
+ if (uri == null) {
+ throw new IllegalStateException("loadThumbnail sent null list for uri " + uri);
+ }
mHandler.post(new Runnable() {
@Override
public void run() {
- Bitmap bitmap = onGetThumbnail(uri, width, height);
- try {
- callbacks.onLoadThumbnail(uri, width, height, bitmap);
- } catch (RemoteException e) {
- Log.e(TAG, "RemoteException in calling onLoadThumbnail", e);
+ // In theory we could return a result to a new connection, but in practice
+ // the other side in MediaBrowser uses a new IMediaBrowserServiceCallbacks
+ // object every time it calls connect(), so as long as it does that we won't
+ // see results sent for the wrong connection.
+ final ConnectionRecord connection = mConnections.get(callbacks.asBinder());
+ if (connection == null) {
+ if (DBG) {
+ Log.d(TAG, "Not loading bitmap for invalid connection. uri=" + uri);
+ }
+ return;
+ }
+
+ final Result<Bitmap> result = new Result<Bitmap>(uri) {
+ @Override
+ void onResultSent(Bitmap bitmap) {
+ if (mConnections.get(connection.callbacks.asBinder()) != connection) {
+ if (DBG) {
+ Log.d(TAG, "Not sending onLoadThumbnail result for connection"
+ + " that has been disconnected. pkg=" + connection.pkg
+ + " uri=" + uri);
+ }
+ return;
+ }
+
+ try {
+ callbacks.onLoadThumbnail(uri, width, height, bitmap);
+ } catch (RemoteException e) {
+ // The other side is in the process of crashing.
+ Log.w(TAG, "RemoteException in calling onLoadThumbnail", e);
+ }
+ }
+ };
+
+ onLoadThumbnail(uri, width, height, result);
+
+ if (!result.isDone()) {
+ throw new IllegalStateException("onLoadThumbnail must call detach() or"
+ + " sendResult() before returning for package=" + connection.pkg
+ + " uri=" + uri);
}
}
});
@@ -261,15 +360,28 @@ public abstract class MediaBrowserService extends Service {
/**
* Called to get information about the children of a media item.
+ * <p>
+ * Implementations must call result.{@link Result#sendResult result.sendResult} with the list
+ * of children. If loading the children will be an expensive operation that should be performed
+ * on another thread, result.{@link Result#detach result.detach} may be called before returning
+ * from this function, and then {@link Result#sendResult result.sendResult} called when
+ * the loading is complete.
*
* @param parentUri The uri of the parent media item whose
* children are to be queried.
* @return The list of children, or null if the uri is invalid.
*/
- public abstract @Nullable List<MediaBrowserItem> onLoadChildren(@NonNull Uri parentUri);
+ protected abstract void onLoadChildren(@NonNull Uri parentUri,
+ @NonNull Result<List<MediaBrowserItem>> result);
/**
* Called to get the thumbnail of a particular media item.
+ * <p>
+ * Implementations must call result.{@link Result#sendResult result.sendResult} with the bitmap.
+ * If loading the bitmap will be an expensive operation that should be performed
+ * on another thread, result.{@link Result#detach result.detach} may be called before returning
+ * from this function, and then {@link Result#sendResult result.sendResult} called when
+ * the loading is complete.
*
* @param uri The uri of the media item.
* @param width The requested width of the icon in dp.
@@ -278,7 +390,8 @@ public abstract class MediaBrowserService extends Service {
* @return The file descriptor of the thumbnail, which may then be loaded
* using a bitmap factory, or null if the item does not have a thumbnail.
*/
- public abstract @Nullable Bitmap onGetThumbnail(@NonNull Uri uri, int width, int height);
+ protected abstract void onLoadThumbnail(@NonNull Uri uri, int width, int height,
+ @NonNull Result<Bitmap> result);
/**
* Call to set the media session.
@@ -363,21 +476,38 @@ public abstract class MediaBrowserService extends Service {
* Call onLoadChildren and then send the results back to the connection.
* <p>
* Callers must make sure that this connection is still connected.
- * <p>
- * TODO: Think about caching and combining these calls.
*/
- private void performLoadChildren(Uri uri, ConnectionRecord connection) {
- final List<MediaBrowserItem> list = onLoadChildren(uri);
- if (list == null) {
- throw new IllegalStateException("onLoadChildren returned null for uri " + uri);
- }
- final ParceledListSlice<MediaBrowserItem> pls = new ParceledListSlice(list);
- try {
- connection.callbacks.onLoadChildren(uri, pls);
- } catch (RemoteException ex) {
- // The other side is in the process of crashing.
- Log.w(TAG, "Calling onLoadChildren() failed for uri=" + uri
- + " package=" + connection.pkg);
+ private void performLoadChildren(final Uri uri, final ConnectionRecord connection) {
+ final Result<List<MediaBrowserItem>> result = new Result<List<MediaBrowserItem>>(uri) {
+ @Override
+ void onResultSent(List<MediaBrowserItem> list) {
+ if (list == null) {
+ throw new IllegalStateException("onLoadChildren sent null list for uri " + uri);
+ }
+ if (mConnections.get(connection.callbacks.asBinder()) != connection) {
+ if (DBG) {
+ Log.d(TAG, "Not sending onLoadChildren result for connection that has"
+ + " been disconnected. pkg=" + connection.pkg + " uri=" + uri);
+ }
+ return;
+ }
+
+ final ParceledListSlice<MediaBrowserItem> pls = new ParceledListSlice(list);
+ try {
+ connection.callbacks.onLoadChildren(uri, pls);
+ } catch (RemoteException ex) {
+ // The other side is in the process of crashing.
+ Log.w(TAG, "Calling onLoadChildren() failed for uri=" + uri
+ + " package=" + connection.pkg);
+ }
+ }
+ };
+
+ onLoadChildren(uri, result);
+
+ if (!result.isDone()) {
+ throw new IllegalStateException("onLoadChildren must call detach() or sendResult()"
+ + " before returning for package=" + connection.pkg + " uri=" + uri);
}
}
diff --git a/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/BrowserService.java b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/BrowserService.java
index e5216b563580..0e7fe133d03d 100644
--- a/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/BrowserService.java
+++ b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/BrowserService.java
@@ -121,23 +121,29 @@ public class BrowserService extends MediaBrowserService {
}
@Override
- protected List<MediaBrowserItem> onLoadChildren(Uri parentUri) {
- final ArrayList<MediaBrowserItem> results = new ArrayList();
-
- for (int i=0; i<10; i++) {
- results.add(new MediaBrowserItem.Builder(
- Uri.withAppendedPath(BASE_URI, Integer.toString(i)),
- MediaBrowserItem.FLAG_BROWSABLE, "Title " + i)
- .setSummary("Summary " + i)
- .build());
- }
-
- return results;
+ protected void onLoadChildren(final Uri parentUri,
+ final Result<List<MediaBrowserItem>> result) {
+ new Handler().postDelayed(new Runnable() {
+ public void run() {
+ final ArrayList<MediaBrowserItem> list = new ArrayList();
+
+ for (int i=0; i<10; i++) {
+ list.add(new MediaBrowserItem.Builder(
+ Uri.withAppendedPath(BASE_URI, Integer.toString(i)),
+ MediaBrowserItem.FLAG_BROWSABLE, "Title " + i)
+ .setSummary("Summary " + i)
+ .build());
+ }
+
+ result.sendResult(list);
+ }
+ }, 2000);
+ result.detach();
}
@Override
- protected Bitmap onGetThumbnail(Uri uri, int width, int height) {
- return null;
+ protected void onLoadThumbnail(Uri uri, int width, int height, Result<Bitmap> result) {
+ result.sendResult(null);
}
/*