Simplify threading for media3 session callbacks

This change removes the requirement that callback implementations
need to be able to handle two specific callbacks being called on
two different threads.

PiperOrigin-RevId: 413958545
This commit is contained in:
olly 2021-12-03 17:52:35 +00:00 committed by Ian Baker
parent 546d9f592b
commit 41a2f9a6b3
4 changed files with 57 additions and 30 deletions

View File

@ -148,10 +148,12 @@ public abstract class MediaLibraryService extends MediaSessionService {
* MediaItem#mediaId}. The media id is required for the browser to get the children under the * MediaItem#mediaId}. The media id is required for the browser to get the children under the
* root. * root.
* *
* <p>Interoperability: This method may be called on the main thread regardless of the * <p>Interoperability: If this callback is called because a legacy {@link
* application thread if legacy {@link android.support.v4.media.MediaBrowserCompat} requested * android.support.v4.media.MediaBrowserCompat} has requested a {@link
* a {@link androidx.media.MediaBrowserServiceCompat.BrowserRoot}. In this case, you must * androidx.media.MediaBrowserServiceCompat.BrowserRoot}, then the main thread may be blocked
* return a completed future. * until the returned future is done. If your service may be queried by a legacy {@link
* android.support.v4.media.MediaBrowserCompat}, you should ensure that the future completes
* quickly to avoid blocking the main thread for a long period of time.
* *
* @param session The session for this event. * @param session The session for this event.
* @param browser The browser information. * @param browser The browser information.

View File

@ -35,6 +35,7 @@ import androidx.core.util.ObjectsCompat;
import androidx.media.MediaBrowserServiceCompat; import androidx.media.MediaBrowserServiceCompat;
import androidx.media.MediaSessionManager.RemoteUserInfo; import androidx.media.MediaSessionManager.RemoteUserInfo;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.util.ConditionVariable;
import androidx.media3.common.util.Log; import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.session.MediaLibraryService.LibraryParams; import androidx.media3.session.MediaLibraryService.LibraryParams;
@ -49,6 +50,7 @@ import java.util.List;
import java.util.concurrent.CancellationException; import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
/** /**
* Implementation of {@link MediaBrowserServiceCompat} for interoperability between {@link * Implementation of {@link MediaBrowserServiceCompat} for interoperability between {@link
@ -89,22 +91,24 @@ import java.util.concurrent.Future;
@Nullable @Nullable
LibraryParams params = LibraryParams params =
MediaUtils.convertToLibraryParams(librarySessionImpl.getContext(), rootHints); MediaUtils.convertToLibraryParams(librarySessionImpl.getContext(), rootHints);
// Call onGetLibraryRoot() directly instead of posting to the application thread not to block AtomicReference<ListenableFuture<LibraryResult<MediaItem>>> futureReference =
// the main thread as MediaBrowserServiceCompat requires to return browser root here. new AtomicReference<>();
// onGetLibraryRoot() has documentation that it may be called on the main thread. ConditionVariable haveFuture = new ConditionVariable();
ListenableFuture<LibraryResult<MediaItem>> future = postOrRun(
librarySessionImpl.getApplicationHandler(),
() -> {
futureReference.set(
checkNotNull( checkNotNull(
librarySessionImpl librarySessionImpl
.getCallback() .getCallback()
.onGetLibraryRoot(librarySessionImpl.getInstance(), controller, params), .onGetLibraryRoot(librarySessionImpl.getInstance(), controller, params),
"onGetLibraryRoot must return non-null future"); "onGetLibraryRoot must return non-null future"));
if (!future.isDone()) { haveFuture.open();
throw new RuntimeException( });
"onGetLibraryRoot must return a completed future " + "for legacy MediaBrowserCompat");
}
@Nullable LibraryResult<MediaItem> result = null; @Nullable LibraryResult<MediaItem> result = null;
try { try {
result = checkNotNull(future.get(), "LibraryResult must not be null"); haveFuture.block();
result = checkNotNull(futureReference.get().get(), "LibraryResult must not be null");
} catch (CancellationException | ExecutionException | InterruptedException e) { } catch (CancellationException | ExecutionException | InterruptedException e) {
Log.e(TAG, "Couldn't get a result from onGetLibraryRoot", e); Log.e(TAG, "Couldn't get a result from onGetLibraryRoot", e);
} }

View File

@ -792,8 +792,10 @@ public class MediaSession {
* #sendCustomCommand}, {@link #setCustomLayout}) will be ignored. Use {@link #onPostConnect} * #sendCustomCommand}, {@link #setCustomLayout}) will be ignored. Use {@link #onPostConnect}
* for custom initialization of the controller instead. * for custom initialization of the controller instead.
* *
* <p>Interoperability: this callback may be called on the main thread, regardless of the * <p>Interoperability: If a legacy controller is connecting to the session then this callback
* application thread. * may block the main thread, even if it's called on a different application thread. If it's
* possible that legacy controllers will connect to the session, you should ensure that the
* callback returns quickly to avoid blocking the main thread for a long period of time.
* *
* @param session The session for this event. * @param session The session for this event.
* @param controller The controller information. * @param controller The controller information.

View File

@ -15,6 +15,9 @@
*/ */
package androidx.media3.session; package androidx.media3.session;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Util.postOrRun;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.media.MediaBrowserCompat.MediaItem; import android.support.v4.media.MediaBrowserCompat.MediaItem;
import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.MediaSessionCompat;
@ -22,8 +25,11 @@ import androidx.annotation.Nullable;
import androidx.media.MediaBrowserServiceCompat; import androidx.media.MediaBrowserServiceCompat;
import androidx.media.MediaSessionManager; import androidx.media.MediaSessionManager;
import androidx.media.MediaSessionManager.RemoteUserInfo; import androidx.media.MediaSessionManager.RemoteUserInfo;
import androidx.media3.common.util.ConditionVariable;
import androidx.media3.common.util.Log;
import androidx.media3.session.MediaSession.ControllerInfo; import androidx.media3.session.MediaSession.ControllerInfo;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
/** /**
* Implementation of {@link MediaBrowserServiceCompat} for interoperability between {@link * Implementation of {@link MediaBrowserServiceCompat} for interoperability between {@link
@ -31,6 +37,8 @@ import java.util.List;
*/ */
/* package */ class MediaSessionServiceLegacyStub extends MediaBrowserServiceCompat { /* package */ class MediaSessionServiceLegacyStub extends MediaBrowserServiceCompat {
private static final String TAG = "MSSLegacyStub";
private final MediaSessionManager manager; private final MediaSessionManager manager;
private final MediaSession.MediaSessionImpl sessionImpl; private final MediaSession.MediaSessionImpl sessionImpl;
private final ConnectedControllersManager<RemoteUserInfo> connectedControllersManager; private final ConnectedControllersManager<RemoteUserInfo> connectedControllersManager;
@ -55,19 +63,30 @@ import java.util.List;
String clientPackageName, int clientUid, @Nullable Bundle rootHints) { String clientPackageName, int clientUid, @Nullable Bundle rootHints) {
RemoteUserInfo info = getCurrentBrowserInfo(); RemoteUserInfo info = getCurrentBrowserInfo();
MediaSession.ControllerInfo controller = createControllerInfo(info); MediaSession.ControllerInfo controller = createControllerInfo(info);
// Call onConnect() directly instead of posting to the application thread not to block the main
// thread as MediaBrowserServiceCompat requires to return browser root here. AtomicReference<MediaSession.ConnectionResult> resultReference = new AtomicReference<>();
// onConnect() has documentation that it may be called on the main thread. ConditionVariable haveResult = new ConditionVariable();
MediaSession.ConnectionResult connectionResult = postOrRun(
sessionImpl.getCallback().onConnect(sessionImpl.getInstance(), controller); sessionImpl.getApplicationHandler(),
if (!connectionResult.isAccepted) { () -> {
resultReference.set(
checkNotNull(
sessionImpl.getCallback().onConnect(sessionImpl.getInstance(), controller),
"onConnect must return non-null future"));
haveResult.open();
});
try {
haveResult.block();
} catch (InterruptedException e) {
Log.e(TAG, "Couldn't get a result from onConnect", e);
return null;
}
MediaSession.ConnectionResult result = resultReference.get();
if (!result.isAccepted) {
return null; return null;
} }
connectedControllersManager.addController( connectedControllersManager.addController(
info, info, controller, result.availableSessionCommands, result.availablePlayerCommands);
controller,
connectionResult.availableSessionCommands,
connectionResult.availablePlayerCommands);
// No library root, but keep browser compat connected to allow getting session. // No library root, but keep browser compat connected to allow getting session.
return MediaUtils.defaultBrowserRoot; return MediaUtils.defaultBrowserRoot;
} }