From 3693ca4bbba68566cc0f4e2dc1e7622b25c783c4 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 26 Apr 2023 16:09:44 +0100 Subject: [PATCH] Add MediaSession.getControllerForCurrentRequest This is a helper method that can used to obtain information about the controller that is currently calling a Player method. PiperOrigin-RevId: 527268994 --- RELEASENOTES.md | 22 ++ api.txt | 1 + .../session/ConnectedControllersManager.java | 30 +- .../androidx/media3/session/MediaSession.java | 22 ++ .../media3/session/MediaSessionImpl.java | 17 + .../session/MediaSessionLegacyStub.java | 72 ++-- .../media3/session/MediaSessionStub.java | 33 +- .../session/MediaSessionPlayerTest.java | 321 ++++++++++++++++++ 8 files changed, 460 insertions(+), 58 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a2fa9f7b4d..5fbcc32d56 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -53,11 +53,17 @@ * Fix issue where `MediaController` doesn't update its available commands when connected to a legacy `MediaSessionCompat` that updates its actions. + * Add helper method `MediaSession.getControllerForCurrentRequest` to + obtain information about the controller that is currently calling + a`Player` method. * UI: + * Add Util methods `shouldShowPlayButton` and `handlePlayPauseButtonAction` to write custom UI elements with a play/pause button. + * Audio: + * Fix bug where some playbacks fail when tunneling is enabled and `AudioProcessors` are active, e.g. for gapless trimming ([#10847](https://github.com/google/ExoPlayer/issues/10847)). @@ -76,10 +82,14 @@ `onRendererCapabilitiesChanged` events. * Add `ChannelMixingAudioProcessor` for applying scaling/mixing to audio channels. + * Metadata: + * Deprecate `MediaMetadata.folderType` in favor of `isBrowsable` and `mediaType`. + * DRM: + * Reduce the visibility of several internal-only methods on `DefaultDrmSession` that aren't expected to be called from outside the DRM package: @@ -87,7 +97,9 @@ * `void provision()` * `void onProvisionCompleted()` * `onProvisionError(Exception, boolean)` + * Transformer: + * Remove `Transformer.Builder.setMediaSourceFactory(MediaSource.Factory)`. Use `ExoPlayerAssetLoader.Factory(MediaSource.Factory)` and `Transformer.Builder.setAssetLoaderFactory(AssetLoader.Factory)` @@ -99,20 +111,30 @@ an input frame was pending processing. * Query codecs via `MediaCodecList` instead of using `findDecoder/EncoderForFormat` utilities, to expand support. + * Muxer: + * Add a new muxer library which can be used to create an MP4 container file. + * DASH: + * Remove the media time offset from `MediaLoadData.startTimeMs` and `MediaLoadData.endTimeMs` for multi period DASH streams. + * RTSP: + * For MPEG4-LATM, use default profile-level-id value if absent in Describe Response SDP message ([#302](https://github.com/androidx/media/issues/302)). + * IMA DAI extension: + * Fix a bug where a new ad group is inserted in live streams because the calculated content position in consecutive timelines varies slightly. + * Remove deprecated symbols: + * Remove `DefaultAudioSink` constructors, use `DefaultAudioSink.Builder` instead. * Remove `HlsMasterPlaylist`, use `HlsMultivariantPlaylist` instead. diff --git a/api.txt b/api.txt index 005870d989..a93a4db773 100644 --- a/api.txt +++ b/api.txt @@ -1704,6 +1704,7 @@ package androidx.media3.session { @com.google.errorprone.annotations.DoNotMock public class MediaSession { method public final void broadcastCustomCommand(androidx.media3.session.SessionCommand, android.os.Bundle); method public final java.util.List getConnectedControllers(); + method @Nullable public final androidx.media3.session.MediaSession.ControllerInfo getControllerForCurrentRequest(); method public final String getId(); method public final androidx.media3.common.Player getPlayer(); method @Nullable public final android.app.PendingIntent getSessionActivity(); diff --git a/libraries/session/src/main/java/androidx/media3/session/ConnectedControllersManager.java b/libraries/session/src/main/java/androidx/media3/session/ConnectedControllersManager.java index 64b069effb..40e3d9619c 100644 --- a/libraries/session/src/main/java/androidx/media3/session/ConnectedControllersManager.java +++ b/libraries/session/src/main/java/androidx/media3/session/ConnectedControllersManager.java @@ -259,20 +259,22 @@ import org.checkerframework.checker.nullness.qual.NonNull; AtomicBoolean commandExecuting = new AtomicBoolean(true); postOrRun( sessionImpl.getApplicationHandler(), - () -> - asyncCommand - .run() - .addListener( - () -> { - synchronized (lock) { - if (!commandExecuting.get()) { - flushCommandQueue(info); - } else { - continueRunning.set(true); - } - } - }, - MoreExecutors.directExecutor())); + sessionImpl.callWithControllerForCurrentRequestSet( + getController(info.controllerKey), + () -> + asyncCommand + .run() + .addListener( + () -> { + synchronized (lock) { + if (!commandExecuting.get()) { + flushCommandQueue(info); + } else { + continueRunning.set(true); + } + } + }, + MoreExecutors.directExecutor()))); commandExecuting.set(false); } } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index 51812d4226..e4c19abb8f 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -660,6 +660,28 @@ public class MediaSession { return impl.getConnectedControllers(); } + /** + * Returns the {@link ControllerInfo} for the controller that sent the current request for a + * {@link Player} method. + * + *

This method will return a non-null value while {@link Player} methods triggered by a + * controller are executed. + * + *

Note: If you want to prevent a controller from calling a method, specify the {@link + * ConnectionResult#availablePlayerCommands available commands} in {@link Callback#onConnect} or + * set them via {@link #setAvailableCommands}. + * + *

This method must be called on the {@linkplain Player#getApplicationLooper() application + * thread} of the underlying player. + * + * @return The {@link ControllerInfo} of the controller that sent the current request, or {@code + * null} if not applicable. + */ + @Nullable + public final ControllerInfo getControllerForCurrentRequest() { + return impl.getControllerForCurrentRequest(); + } + /** * Requests that controllers set the ordered list of {@link CommandButton} to build UI with it. * diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java index b593447dfd..100d281536 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -39,6 +39,7 @@ import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; import android.support.v4.media.session.MediaSessionCompat; +import androidx.annotation.CheckResult; import androidx.annotation.FloatRange; import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; @@ -123,6 +124,7 @@ import org.checkerframework.checker.initialization.qual.Initialized; private PlayerInfo playerInfo; private PlayerWrapper playerWrapper; + @Nullable private ControllerInfo controllerForCurrentRequest; @GuardedBy("lock") @Nullable @@ -278,6 +280,16 @@ import org.checkerframework.checker.initialization.qual.Initialized; return playerWrapper; } + @CheckResult + public Runnable callWithControllerForCurrentRequestSet( + @Nullable ControllerInfo controllerForCurrentRequest, Runnable runnable) { + return () -> { + this.controllerForCurrentRequest = controllerForCurrentRequest; + runnable.run(); + this.controllerForCurrentRequest = null; + }; + } + public String getId() { return sessionId; } @@ -298,6 +310,11 @@ import org.checkerframework.checker.initialization.qual.Initialized; return controllers; } + @Nullable + public ControllerInfo getControllerForCurrentRequest() { + return controllerForCurrentRequest; + } + public boolean isConnected(ControllerInfo controller) { return sessionStub.getConnectedControllersManager().isConnected(controller) || sessionLegacyStub.getConnectedControllersManager().isConnected(controller); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index e39293a3c5..cefbcbb620 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -655,16 +655,22 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return; } - try { - task.run(controller); - } catch (RemoteException e) { - // Currently it's TransactionTooLargeException or DeadSystemException. - // We'd better to leave log for those cases because - // - TransactionTooLargeException means that we may need to fix our code. - // (e.g. add pagination or special way to deliver Bitmap) - // - DeadSystemException means that errors around it can be ignored. - Log.w(TAG, "Exception in " + controller, e); - } + sessionImpl + .callWithControllerForCurrentRequestSet( + controller, + () -> { + try { + task.run(controller); + } catch (RemoteException e) { + // Currently it's TransactionTooLargeException or DeadSystemException. + // We'd better to leave log for those cases because + // - TransactionTooLargeException means that we may need to fix our code. + // (e.g. add pagination or special way to deliver Bitmap) + // - DeadSystemException means that errors around it can be ignored. + Log.w(TAG, "Exception in " + controller, e); + } + }) + .run(); }); } @@ -788,20 +794,22 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; public void onSuccess(MediaItemsWithStartPosition mediaItemsWithStartPosition) { postOrRun( sessionImpl.getApplicationHandler(), - () -> { - PlayerWrapper player = sessionImpl.getPlayerWrapper(); - MediaUtils.setMediaItemsWithStartIndexAndPosition( - player, mediaItemsWithStartPosition); - @Player.State int playbackState = player.getPlaybackState(); - if (playbackState == Player.STATE_IDLE) { - player.prepareIfCommandAvailable(); - } else if (playbackState == Player.STATE_ENDED) { - player.seekToDefaultPositionIfCommandAvailable(); - } - if (play) { - player.playIfCommandAvailable(); - } - }); + sessionImpl.callWithControllerForCurrentRequestSet( + controller, + () -> { + PlayerWrapper player = sessionImpl.getPlayerWrapper(); + MediaUtils.setMediaItemsWithStartIndexAndPosition( + player, mediaItemsWithStartPosition); + @Player.State int playbackState = player.getPlaybackState(); + if (playbackState == Player.STATE_IDLE) { + player.prepareIfCommandAvailable(); + } else if (playbackState == Player.STATE_ENDED) { + player.seekToDefaultPositionIfCommandAvailable(); + } + if (play) { + player.playIfCommandAvailable(); + } + })); } @Override @@ -836,13 +844,15 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; public void onSuccess(List mediaItems) { postOrRun( sessionImpl.getApplicationHandler(), - () -> { - if (index == C.INDEX_UNSET) { - sessionImpl.getPlayerWrapper().addMediaItems(mediaItems); - } else { - sessionImpl.getPlayerWrapper().addMediaItems(index, mediaItems); - } - }); + sessionImpl.callWithControllerForCurrentRequestSet( + controller, + () -> { + if (index == C.INDEX_UNSET) { + sessionImpl.getPlayerWrapper().addMediaItems(mediaItems); + } else { + sessionImpl.getPlayerWrapper().addMediaItems(index, mediaItems); + } + })); } @Override diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java index ee33cffe39..e5cf3808f1 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -204,12 +204,14 @@ import java.util.concurrent.ExecutionException; mediaItems -> postOrRunWithCompletion( sessionImpl.getApplicationHandler(), - () -> { - if (!sessionImpl.isReleased()) { - mediaItemPlayerTask.run( - sessionImpl.getPlayerWrapper(), controller, mediaItems); - } - }, + sessionImpl.callWithControllerForCurrentRequestSet( + controller, + () -> { + if (!sessionImpl.isReleased()) { + mediaItemPlayerTask.run( + sessionImpl.getPlayerWrapper(), controller, mediaItems); + } + }), new SessionResult(SessionResult.RESULT_SUCCESS))); }; } @@ -228,12 +230,14 @@ import java.util.concurrent.ExecutionException; mediaItemsWithStartPosition -> postOrRunWithCompletion( sessionImpl.getApplicationHandler(), - () -> { - if (!sessionImpl.isReleased()) { - mediaItemPlayerTask.run( - sessionImpl.getPlayerWrapper(), mediaItemsWithStartPosition); - } - }, + sessionImpl.callWithControllerForCurrentRequestSet( + controller, + () -> { + if (!sessionImpl.isReleased()) { + mediaItemPlayerTask.run( + sessionImpl.getPlayerWrapper(), mediaItemsWithStartPosition); + } + }), new SessionResult(SessionResult.RESULT_SUCCESS))); }; } @@ -305,7 +309,10 @@ import java.util.concurrent.ExecutionException; return; } if (command == COMMAND_SET_VIDEO_SURFACE) { - task.run(sessionImpl, controller, sequenceNumber); + sessionImpl + .callWithControllerForCurrentRequestSet( + controller, () -> task.run(sessionImpl, controller, sequenceNumber)) + .run(); } else { connectedControllersManager.addToCommandQueue( controller, () -> task.run(sessionImpl, controller, sequenceNumber)); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java index 35a8d9a9ff..e823adf4f9 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java @@ -19,13 +19,20 @@ import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PA import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS; import static com.google.common.truth.Truth.assertThat; +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; +import android.os.Looper; +import android.support.v4.media.MediaDescriptionCompat; +import android.support.v4.media.session.MediaControllerCompat; import androidx.media3.common.DeviceInfo; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaMetadata; import androidx.media3.common.PlaybackParameters; import androidx.media3.common.Player; +import androidx.media3.common.SimpleBasePlayer; import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.util.Util; import androidx.media3.test.session.common.HandlerThreadTestRule; @@ -38,6 +45,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; import org.junit.After; import org.junit.Before; import org.junit.ClassRule; @@ -836,6 +845,318 @@ public class MediaSessionPlayerTest { assertThat(player.seekMediaItemIndex).isEqualTo(3); } + @Test + public void + getControllerForCurrentRequest_withMediaControllerAndSimplePlayerMethod_returnsControllerFromPlayerMethod() + throws Exception { + AtomicReference controllerInfoFromPlayerMethod = + new AtomicReference<>(); + AtomicReference sessionReference = new AtomicReference<>(); + CountDownLatch eventHandled = new CountDownLatch(1); + Player player = + new SimpleBasePlayer(Looper.getMainLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands(new Commands.Builder().add(Player.COMMAND_PLAY_PAUSE).build()) + .build(); + } + + @Override + protected ListenableFuture handleSetPlayWhenReady(boolean playWhenReady) { + controllerInfoFromPlayerMethod.set( + sessionReference.get().getControllerForCurrentRequest()); + eventHandled.countDown(); + return Futures.immediateVoidFuture(); + } + }; + Context context = ApplicationProvider.getApplicationContext(); + MediaSession session = new MediaSession.Builder(context, player).setId("test").build(); + sessionReference.set(session); + Bundle controllerHints = new Bundle(); + controllerHints.putString("key", "value"); + MediaController controller = + new MediaController.Builder(context, session.getToken()) + .setConnectionHints(controllerHints) + .buildAsync() + .get(); + + MainLooperTestRule.runOnMainSync(controller::play); + eventHandled.await(); + session.release(); + + assertThat(controllerInfoFromPlayerMethod.get().getConnectionHints().getString("key")) + .isEqualTo("value"); + } + + @Test + public void + getControllerForCurrentRequest_withMediaControllerAndAsyncSetMediaItemMethod_returnsControllerFromPlayerMethod() + throws Exception { + AtomicReference controllerInfoFromPlayerMethod = + new AtomicReference<>(); + AtomicReference sessionReference = new AtomicReference<>(); + CountDownLatch eventHandled = new CountDownLatch(1); + Player player = + new SimpleBasePlayer(Looper.getMainLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder().add(Player.COMMAND_SET_MEDIA_ITEM).build()) + .build(); + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + controllerInfoFromPlayerMethod.set( + sessionReference.get().getControllerForCurrentRequest()); + eventHandled.countDown(); + return Futures.immediateVoidFuture(); + } + }; + Context context = ApplicationProvider.getApplicationContext(); + MediaSession session = + new MediaSession.Builder(context, player) + .setId("test") + .setCallback( + new MediaSession.Callback() { + @Override + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, + MediaSession.ControllerInfo controller, + List mediaItems) { + // Resolve media items asynchronously. + return Util.postOrRunWithCompletion( + threadTestRule.getHandler(), () -> {}, mediaItems); + } + }) + .build(); + sessionReference.set(session); + Bundle controllerHints = new Bundle(); + controllerHints.putString("key", "value"); + MediaController controller = + new MediaController.Builder(context, session.getToken()) + .setConnectionHints(controllerHints) + .buildAsync() + .get(); + + MainLooperTestRule.runOnMainSync(() -> controller.setMediaItem(MediaItem.fromUri("test://"))); + eventHandled.await(); + session.release(); + + assertThat(controllerInfoFromPlayerMethod.get().getConnectionHints().getString("key")) + .isEqualTo("value"); + } + + @Test + public void + getControllerForCurrentRequest_withMediaControllerAndAsyncAddMediaItemMethod_returnsControllerFromPlayerMethod() + throws Exception { + AtomicReference controllerInfoFromPlayerMethod = + new AtomicReference<>(); + AtomicReference sessionReference = new AtomicReference<>(); + CountDownLatch eventHandled = new CountDownLatch(1); + Player player = + new SimpleBasePlayer(Looper.getMainLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder().add(Player.COMMAND_CHANGE_MEDIA_ITEMS).build()) + .build(); + } + + @Override + protected ListenableFuture handleAddMediaItems(int index, List mediaItems) { + controllerInfoFromPlayerMethod.set( + sessionReference.get().getControllerForCurrentRequest()); + eventHandled.countDown(); + return Futures.immediateVoidFuture(); + } + }; + Context context = ApplicationProvider.getApplicationContext(); + MediaSession session = + new MediaSession.Builder(context, player) + .setId("test") + .setCallback( + new MediaSession.Callback() { + @Override + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, + MediaSession.ControllerInfo controller, + List mediaItems) { + // Resolve media items asynchronously. + return Util.postOrRunWithCompletion( + threadTestRule.getHandler(), () -> {}, mediaItems); + } + }) + .build(); + sessionReference.set(session); + Bundle controllerHints = new Bundle(); + controllerHints.putString("key", "value"); + MediaController controller = + new MediaController.Builder(context, session.getToken()) + .setConnectionHints(controllerHints) + .buildAsync() + .get(); + + MainLooperTestRule.runOnMainSync(() -> controller.addMediaItem(MediaItem.fromUri("test://"))); + eventHandled.await(); + session.release(); + + assertThat(controllerInfoFromPlayerMethod.get().getConnectionHints().getString("key")) + .isEqualTo("value"); + } + + @Test + public void + getControllerForCurrentRequest_withMediaControllerCompatAndSimplePlayerMethod_returnsControllerFromPlayerMethod() + throws Exception { + AtomicReference controllerInfoFromPlayerMethod = + new AtomicReference<>(); + AtomicReference sessionReference = new AtomicReference<>(); + CountDownLatch eventHandled = new CountDownLatch(1); + Player player = + new SimpleBasePlayer(Looper.getMainLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands(new Commands.Builder().add(Player.COMMAND_PLAY_PAUSE).build()) + .build(); + } + + @Override + protected ListenableFuture handleSetPlayWhenReady(boolean playWhenReady) { + controllerInfoFromPlayerMethod.set( + sessionReference.get().getControllerForCurrentRequest()); + eventHandled.countDown(); + return Futures.immediateVoidFuture(); + } + }; + Context context = ApplicationProvider.getApplicationContext(); + MediaSession session = new MediaSession.Builder(context, player).setId("test").build(); + sessionReference.set(session); + MediaControllerCompat controller = session.getSessionCompat().getController(); + + controller.getTransportControls().play(); + eventHandled.await(); + session.release(); + + assertThat(controllerInfoFromPlayerMethod.get().getInterfaceVersion()).isEqualTo(0); + } + + @Test + public void + getControllerForCurrentRequest_withMediaControllerCompatAndAsyncPlayFromMethod_returnsControllerFromPlayerMethod() + throws Exception { + AtomicReference controllerInfoFromPlayerMethod = + new AtomicReference<>(); + AtomicReference sessionReference = new AtomicReference<>(); + CountDownLatch eventHandled = new CountDownLatch(1); + Player player = + new SimpleBasePlayer(Looper.getMainLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder().add(Player.COMMAND_SET_MEDIA_ITEM).build()) + .build(); + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + controllerInfoFromPlayerMethod.set( + sessionReference.get().getControllerForCurrentRequest()); + eventHandled.countDown(); + return Futures.immediateVoidFuture(); + } + }; + Context context = ApplicationProvider.getApplicationContext(); + MediaSession session = + new MediaSession.Builder(context, player) + .setId("test") + .setCallback( + new MediaSession.Callback() { + @Override + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, + MediaSession.ControllerInfo controller, + List mediaItems) { + // Resolve media items asynchronously. + return Util.postOrRunWithCompletion( + threadTestRule.getHandler(), () -> {}, mediaItems); + } + }) + .build(); + sessionReference.set(session); + MediaControllerCompat controller = session.getSessionCompat().getController(); + + controller.getTransportControls().playFromUri(Uri.parse("test://"), Bundle.EMPTY); + eventHandled.await(); + session.release(); + + assertThat(controllerInfoFromPlayerMethod.get().getInterfaceVersion()).isEqualTo(0); + } + + @Test + public void + getControllerForCurrentRequest_withMediaControllerCompatAndAsyncAddToQueueMethod_returnsControllerFromPlayerMethod() + throws Exception { + AtomicReference controllerInfoFromPlayerMethod = + new AtomicReference<>(); + AtomicReference sessionReference = new AtomicReference<>(); + CountDownLatch eventHandled = new CountDownLatch(1); + Player player = + new SimpleBasePlayer(Looper.getMainLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder().add(Player.COMMAND_CHANGE_MEDIA_ITEMS).build()) + .build(); + } + + @Override + protected ListenableFuture handleAddMediaItems(int index, List mediaItems) { + controllerInfoFromPlayerMethod.set( + sessionReference.get().getControllerForCurrentRequest()); + eventHandled.countDown(); + return Futures.immediateVoidFuture(); + } + }; + Context context = ApplicationProvider.getApplicationContext(); + MediaSession session = + new MediaSession.Builder(context, player) + .setId("test") + .setCallback( + new MediaSession.Callback() { + @Override + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, + MediaSession.ControllerInfo controller, + List mediaItems) { + // Resolve media items asynchronously. + return Util.postOrRunWithCompletion( + threadTestRule.getHandler(), () -> {}, mediaItems); + } + }) + .build(); + sessionReference.set(session); + + MainLooperTestRule.runOnMainSync( + () -> { + MediaControllerCompat controller = session.getSessionCompat().getController(); + controller.addQueueItem(new MediaDescriptionCompat.Builder().setMediaId("id").build()); + }); + eventHandled.await(); + session.release(); + + assertThat(controllerInfoFromPlayerMethod.get().getInterfaceVersion()).isEqualTo(0); + } + private void changePlaybackTypeToRemote() throws Exception { threadTestRule .getHandler()