diff options
author | 2014-07-22 23:21:12 -0700 | |
---|---|---|
committer | 2014-07-23 17:56:57 -0700 | |
commit | 69b078599b8d8bc3e8f94d6cab881145f4e2c129 (patch) | |
tree | a5e05fdfe4dd495b414a1e6f1f829ae1011f57f7 | |
parent | 77bfc8f22892227ebc42afd282bd044742956be2 (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.txt | 9 | ||||
-rw-r--r-- | media/java/android/media/browse/MediaBrowserService.java | 178 | ||||
-rw-r--r-- | tests/MusicServiceDemo/src/com/example/android/musicservicedemo/BrowserService.java | 34 |
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); } /* |