diff --git a/libraries/session/src/main/java/androidx/media3/session/DefaultActionFactory.java b/libraries/session/src/main/java/androidx/media3/session/DefaultActionFactory.java index 646571cfd0..ed65296b88 100644 --- a/libraries/session/src/main/java/androidx/media3/session/DefaultActionFactory.java +++ b/libraries/session/src/main/java/androidx/media3/session/DefaultActionFactory.java @@ -56,19 +56,6 @@ import androidx.media3.common.util.Util; public static final String EXTRAS_KEY_ACTION_CUSTOM_EXTRAS = "androidx.media3.session.EXTRAS_KEY_CUSTOM_NOTIFICATION_ACTION_EXTRAS"; - /** - * Returns the {@link KeyEvent} that was included in the media action, or {@code null} if no - * {@link KeyEvent} is found in the {@code intent}. - */ - @Nullable - public static KeyEvent getKeyEvent(Intent intent) { - @Nullable Bundle extras = intent.getExtras(); - if (extras != null && extras.containsKey(Intent.EXTRA_KEY_EVENT)) { - return extras.getParcelable(Intent.EXTRA_KEY_EVENT); - } - return null; - } - private final Service service; private int customActionPendingIntentRequestCode = 0; @@ -110,7 +97,6 @@ import androidx.media3.common.util.Util; mediaSession, customCommand.customAction, customCommand.customExtras)); } - @SuppressWarnings("PendingIntentMutability") // We can't use SaferPendingIntent @Override public PendingIntent createMediaActionPendingIntent( MediaSession mediaSession, @Player.Command long command) { @@ -150,7 +136,6 @@ import androidx.media3.common.util.Util; return KEYCODE_UNKNOWN; } - @SuppressWarnings("PendingIntentMutability") // We can't use SaferPendingIntent private PendingIntent createCustomActionPendingIntent( MediaSession mediaSession, String action, Bundle extras) { Intent intent = new Intent(ACTION_CUSTOM); @@ -177,6 +162,19 @@ import androidx.media3.common.util.Util; return ACTION_CUSTOM.equals(intent.getAction()); } + /** + * Returns the {@link KeyEvent} that was included in the media action, or {@code null} if no + * {@link KeyEvent} is found in the {@code intent}. + */ + @Nullable + public KeyEvent getKeyEvent(Intent intent) { + @Nullable Bundle extras = intent.getExtras(); + if (extras != null && extras.containsKey(Intent.EXTRA_KEY_EVENT)) { + return extras.getParcelable(Intent.EXTRA_KEY_EVENT); + } + return null; + } + /** * Returns the custom action that was included in the {@link #createCustomAction custom action}, * or {@code null} if no custom action is found in the {@code intent}. @@ -203,7 +201,6 @@ import androidx.media3.common.util.Util; private static final class Api26 { private Api26() {} - @SuppressWarnings("PendingIntentMutability") // We can't use SaferPendingIntent public static PendingIntent createForegroundServicePendingIntent( Service service, int keyCode, Intent intent) { return PendingIntent.getForegroundService( diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java b/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java index 4eaa932d9d..8093161bd8 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java @@ -17,6 +17,14 @@ package androidx.media3.session; import static android.app.Service.STOP_FOREGROUND_DETACH; import static android.app.Service.STOP_FOREGROUND_REMOVE; +import static android.view.KeyEvent.KEYCODE_MEDIA_FAST_FORWARD; +import static android.view.KeyEvent.KEYCODE_MEDIA_NEXT; +import static android.view.KeyEvent.KEYCODE_MEDIA_PAUSE; +import static android.view.KeyEvent.KEYCODE_MEDIA_PLAY; +import static android.view.KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE; +import static android.view.KeyEvent.KEYCODE_MEDIA_PREVIOUS; +import static android.view.KeyEvent.KEYCODE_MEDIA_REWIND; +import static android.view.KeyEvent.KEYCODE_MEDIA_STOP; import static java.util.concurrent.TimeUnit.MILLISECONDS; import android.annotation.SuppressLint; @@ -26,6 +34,7 @@ import android.content.pm.ServiceInfo; import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import android.view.KeyEvent; import androidx.annotation.DoNotInline; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; @@ -65,7 +74,7 @@ import java.util.concurrent.TimeoutException; private final NotificationManagerCompat notificationManagerCompat; private final Executor mainExecutor; private final Intent startSelfIntent; - private final Map> controllerMap; + private final Map controllerAndListenerMap; private int totalNotificationCount; @Nullable private MediaNotification mediaNotification; @@ -82,30 +91,34 @@ import java.util.concurrent.TimeoutException; Handler mainHandler = new Handler(Looper.getMainLooper()); mainExecutor = (runnable) -> Util.postOrRun(mainHandler, runnable); startSelfIntent = new Intent(mediaSessionService, mediaSessionService.getClass()); - controllerMap = new HashMap<>(); + controllerAndListenerMap = new HashMap<>(); startedInForeground = false; } public void addSession(MediaSession session) { - if (controllerMap.containsKey(session)) { + if (controllerAndListenerMap.containsKey(session)) { return; } - MediaControllerListener listener = new MediaControllerListener(mediaSessionService, session); + MediaControllerListener controllerListener = + new MediaControllerListener(mediaSessionService, session); + PlayerListener playerListener = new PlayerListener(mediaSessionService, session); Bundle connectionHints = new Bundle(); connectionHints.putBoolean(KEY_MEDIA_NOTIFICATION_MANAGER, true); ListenableFuture controllerFuture = new MediaController.Builder(mediaSessionService, session.getToken()) .setConnectionHints(connectionHints) - .setListener(listener) + .setListener(controllerListener) .setApplicationLooper(Looper.getMainLooper()) .buildAsync(); - controllerMap.put(session, controllerFuture); + controllerAndListenerMap.put( + session, new ControllerAndListener(controllerFuture, playerListener)); controllerFuture.addListener( () -> { try { - MediaController controller = controllerFuture.get(/* time= */ 0, MILLISECONDS); - listener.onConnected(shouldShowNotification(session)); - controller.addListener(listener); + // Assert connection success. + controllerFuture.get(/* time= */ 0, MILLISECONDS); + controllerListener.onConnected(shouldShowNotification(session)); + session.getImpl().addPlayerListener(playerListener); } catch (CancellationException | ExecutionException | InterruptedException @@ -118,9 +131,52 @@ import java.util.concurrent.TimeoutException; } public void removeSession(MediaSession session) { - @Nullable ListenableFuture future = controllerMap.remove(session); - if (future != null) { - MediaController.releaseFuture(future); + ControllerAndListener controllerAndListener = controllerAndListenerMap.remove(session); + if (controllerAndListener != null) { + session.getImpl().removePlayerListener(controllerAndListener.listener); + MediaController.releaseFuture(controllerAndListener.controller); + } + } + + public void onMediaButtonEvent(MediaSession session, KeyEvent keyEvent) { + int keyCode = keyEvent.getKeyCode(); + @Nullable MediaController mediaController = getConnectedControllerForSession(session); + if (mediaController == null) { + session.getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent); + return; + } + switch (keyCode) { + case KEYCODE_MEDIA_PLAY_PAUSE: + if (mediaController.getPlayWhenReady()) { + mediaController.pause(); + } else { + mediaController.play(); + } + break; + case KEYCODE_MEDIA_PLAY: + mediaController.play(); + break; + case KEYCODE_MEDIA_PAUSE: + mediaController.pause(); + break; + case KEYCODE_MEDIA_NEXT: + mediaController.seekToNext(); + break; + case KEYCODE_MEDIA_PREVIOUS: + mediaController.seekToPrevious(); + break; + case KEYCODE_MEDIA_FAST_FORWARD: + mediaController.seekForward(); + break; + case KEYCODE_MEDIA_REWIND: + mediaController.seekBack(); + break; + case KEYCODE_MEDIA_STOP: + mediaController.stop(); + break; + default: + Log.w(TAG, "Received media button event with unsupported key code: " + keyCode); + break; } } @@ -154,11 +210,11 @@ import java.util.concurrent.TimeoutException; int notificationSequence = ++totalNotificationCount; MediaController mediaNotificationController = null; - ListenableFuture controller = controllerMap.get(session); - if (controller != null && controller.isDone()) { + ControllerAndListener controllerAndListener = controllerAndListenerMap.get(session); + if (controllerAndListener != null && controllerAndListener.controller.isDone()) { try { - mediaNotificationController = Futures.getDone(controller); - } catch (ExecutionException e) { + mediaNotificationController = Futures.getDone(controllerAndListener.controller); + } catch (CancellationException | ExecutionException e) { // Ignore. } } @@ -261,13 +317,13 @@ import java.util.concurrent.TimeoutException; @Nullable private MediaController getConnectedControllerForSession(MediaSession session) { - ListenableFuture controller = controllerMap.get(session); - if (controller == null) { + ControllerAndListener controllerAndListener = controllerAndListenerMap.get(session); + if (controllerAndListener == null) { return null; } try { - return Futures.getDone(controller); - } catch (ExecutionException exception) { + return Futures.getDone(controllerAndListener.controller); + } catch (CancellationException | ExecutionException exception) { // We should never reach this. throw new IllegalStateException(exception); } @@ -305,8 +361,7 @@ import java.util.concurrent.TimeoutException; } } - private static final class MediaControllerListener - implements MediaController.Listener, Player.Listener { + private static final class MediaControllerListener implements MediaController.Listener { private final MediaSessionService mediaSessionService; private final MediaSession session; @@ -344,6 +399,18 @@ import java.util.concurrent.TimeoutException; mediaSessionService.onUpdateNotificationInternal( session, /* startInForegroundWhenPaused= */ false); } + } + + private static class PlayerListener implements Player.Listener { + private final MediaSessionService mediaSessionService; + private final MediaSession session; + private final Handler mainHandler; + + public PlayerListener(MediaSessionService mediaSessionService, MediaSession session) { + this.mediaSessionService = mediaSessionService; + this.session = session; + mainHandler = new Handler(Looper.getMainLooper()); + } @Override public void onEvents(Player player, Player.Events events) { @@ -354,8 +421,13 @@ import java.util.concurrent.TimeoutException; Player.EVENT_PLAY_WHEN_READY_CHANGED, Player.EVENT_MEDIA_METADATA_CHANGED, Player.EVENT_TIMELINE_CHANGED)) { - mediaSessionService.onUpdateNotificationInternal( - session, /* startInForegroundWhenPaused= */ false); + // onUpdateNotificationInternal is required to be called on the main thread and the + // application thread of the player may be a different thread. + Util.postOrRun( + mainHandler, + () -> + mediaSessionService.onUpdateNotificationInternal( + session, /* startInForegroundWhenPaused= */ false)); } } } @@ -385,6 +457,17 @@ import java.util.concurrent.TimeoutException; startedInForeground = false; } + private static class ControllerAndListener { + public final ListenableFuture controller; + public final Player.Listener listener; + + private ControllerAndListener( + ListenableFuture controller, Player.Listener listener) { + this.controller = controller; + this.listener = listener; + } + } + @RequiresApi(24) private static class Api24 { 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 bb5dedbcff..29efaaa2ab 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -15,28 +15,15 @@ */ package androidx.media3.session; -import static android.view.KeyEvent.KEYCODE_MEDIA_FAST_FORWARD; -import static android.view.KeyEvent.KEYCODE_MEDIA_NEXT; -import static android.view.KeyEvent.KEYCODE_MEDIA_PAUSE; -import static android.view.KeyEvent.KEYCODE_MEDIA_PLAY; -import static android.view.KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE; -import static android.view.KeyEvent.KEYCODE_MEDIA_PREVIOUS; -import static android.view.KeyEvent.KEYCODE_MEDIA_REWIND; -import static android.view.KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD; -import static android.view.KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD; -import static android.view.KeyEvent.KEYCODE_MEDIA_STOP; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull; -import static androidx.media3.common.util.Util.SDK_INT; import static androidx.media3.common.util.Util.postOrRun; -import static androidx.media3.session.MediaSessionStub.UNKNOWN_SEQUENCE_NUMBER; import static androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_DISCONNECTED; import static androidx.media3.session.SessionResult.RESULT_ERROR_UNKNOWN; import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED; import static java.lang.Math.min; import android.app.PendingIntent; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.net.Uri; @@ -50,7 +37,6 @@ import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; import android.support.v4.media.session.MediaSessionCompat; -import android.view.KeyEvent; import androidx.annotation.CheckResult; import androidx.annotation.FloatRange; import androidx.annotation.GuardedBy; @@ -148,6 +134,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private boolean closed; // Should be only accessed on the application looper + private final List wrapperListeners; private long sessionPositionUpdateDelayMs; private boolean isMediaNotificationControllerConnected; private ImmutableList customLayout; @@ -174,6 +161,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; sessionStub = new MediaSessionStub(thisRef); this.sessionActivity = sessionActivity; this.customLayout = customLayout; + wrapperListeners = new ArrayList<>(); mainHandler = new Handler(Looper.getMainLooper()); applicationHandler = new Handler(player.getApplicationLooper()); @@ -252,14 +240,38 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; playerWrapper.getAvailablePlayerCommands())); } + public void addPlayerListener(Player.Listener listener) { + postOrRun( + applicationHandler, + () -> { + wrapperListeners.add(listener); + playerWrapper.addListener(listener); + }); + } + + public void removePlayerListener(Player.Listener listener) { + postOrRun( + applicationHandler, + () -> { + playerWrapper.removeListener(listener); + wrapperListeners.remove(listener); + }); + } + private void setPlayerInternal( @Nullable PlayerWrapper oldPlayerWrapper, PlayerWrapper newPlayerWrapper) { playerWrapper = newPlayerWrapper; if (oldPlayerWrapper != null) { oldPlayerWrapper.removeListener(checkStateNotNull(this.playerListener)); + for (int i = 0; i < wrapperListeners.size(); i++) { + oldPlayerWrapper.removeListener(wrapperListeners.get(i)); + } } PlayerListener playerListener = new PlayerListener(this, newPlayerWrapper); newPlayerWrapper.addListener(playerListener); + for (int i = 0; i < wrapperListeners.size(); i++) { + newPlayerWrapper.addListener(wrapperListeners.get(i)); + } this.playerListener = playerListener; dispatchRemoteControllerTaskToLegacyStub( @@ -291,6 +303,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; if (playerListener != null) { playerWrapper.removeListener(playerListener); } + for (int i = 0; i < wrapperListeners.size(); i++) { + playerWrapper.removeListener(wrapperListeners.get(i)); + } + wrapperListeners.clear(); }); } catch (Exception e) { // Catch all exceptions to ensure the rest of this method to be executed as exceptions may be @@ -1073,75 +1089,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; (callback, seq) -> callback.onDeviceInfoChanged(seq, playerInfo.deviceInfo)); } - /* package */ boolean onMediaButtonEvent(Intent intent) { - KeyEvent keyEvent = DefaultActionFactory.getKeyEvent(intent); - ComponentName intentComponent = intent.getComponent(); - if (!Objects.equals(intent.getAction(), Intent.ACTION_MEDIA_BUTTON) - || (intentComponent != null - && !Objects.equals(intentComponent.getPackageName(), context.getPackageName())) - || keyEvent == null - || keyEvent.getAction() != KeyEvent.ACTION_DOWN) { - return false; - } - ControllerInfo controllerInfo = getMediaNotificationControllerInfo(); - if (controllerInfo == null) { - if (intentComponent != null) { - // Fallback to legacy if this is a media button event sent to one of our components. - return getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent) - || SDK_INT < 21; - } - return false; - } - - Runnable command; - switch (keyEvent.getKeyCode()) { - case KEYCODE_MEDIA_PLAY_PAUSE: - command = - getPlayerWrapper().getPlayWhenReady() - ? () -> sessionStub.pauseForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER) - : () -> sessionStub.playForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER); - break; - case KEYCODE_MEDIA_PLAY: - command = () -> sessionStub.playForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER); - break; - case KEYCODE_MEDIA_PAUSE: - command = () -> sessionStub.pauseForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER); - break; - case KEYCODE_MEDIA_NEXT: // Fall through. - case KEYCODE_MEDIA_SKIP_FORWARD: - command = - () -> sessionStub.seekToNextForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER); - break; - case KEYCODE_MEDIA_PREVIOUS: // Fall through. - case KEYCODE_MEDIA_SKIP_BACKWARD: - command = - () -> - sessionStub.seekToPreviousForControllerInfo( - controllerInfo, UNKNOWN_SEQUENCE_NUMBER); - break; - case KEYCODE_MEDIA_FAST_FORWARD: - command = - () -> sessionStub.seekForwardForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER); - break; - case KEYCODE_MEDIA_REWIND: - command = - () -> sessionStub.seekBackForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER); - break; - case KEYCODE_MEDIA_STOP: - command = () -> sessionStub.stopForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER); - break; - default: - return false; - } - postOrRun( - getApplicationHandler(), - () -> { - command.run(); - sessionStub.getConnectedControllersManager().flushCommandQueue(controllerInfo); - }); - return true; - } - /* @FunctionalInterface */ interface RemoteControllerTask { diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java index 7c30eebfaa..9e87bb2462 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java @@ -31,6 +31,7 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; +import android.view.KeyEvent; import androidx.annotation.CallSuper; import androidx.annotation.DoNotInline; import androidx.annotation.GuardedBy; @@ -156,7 +157,7 @@ public abstract class MediaSessionService extends Service { /** The action for {@link Intent} filter that must be declared by the service. */ public static final String SERVICE_INTERFACE = "androidx.media3.session.MediaSessionService"; - private static final String TAG = "MSessionService"; + private static final String TAG = "MSSImpl"; private final Object lock; private final Handler mainHandler; @@ -425,8 +426,9 @@ public abstract class MediaSessionService extends Service { } addSession(session); } - if (!session.getImpl().onMediaButtonEvent(intent)) { - Log.w(TAG, "Ignoring unrecognized media button intent."); + @Nullable KeyEvent keyEvent = actionFactory.getKeyEvent(intent); + if (keyEvent != null) { + getMediaNotificationManager().onMediaButtonEvent(session, keyEvent); } } else if (session != null && actionFactory.isCustomAction(intent)) { @Nullable String customAction = actionFactory.getCustomAction(intent); diff --git a/libraries/session/src/test/java/androidx/media3/session/MediaSessionServiceTest.java b/libraries/session/src/test/java/androidx/media3/session/MediaSessionServiceTest.java index f6bb52666f..38a3e5b4a4 100644 --- a/libraries/session/src/test/java/androidx/media3/session/MediaSessionServiceTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/MediaSessionServiceTest.java @@ -39,7 +39,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; @@ -412,9 +411,6 @@ public class MediaSessionServiceTest { serviceController.startCommand(/* flags= */ 0, /* startId= */ 0); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(service.callers).hasSize(1); - assertThat(service.session.isMediaNotificationController(service.callers.get(0))).isTrue(); - controller.release(); serviceController.destroy(); } @@ -505,35 +501,21 @@ public class MediaSessionServiceTest { private static final class TestServiceWithPlaybackResumption extends MediaSessionService { - private final List callers; - - private ImmutableList mediaItems; - @Nullable private MediaSession session; - - public TestServiceWithPlaybackResumption() { - callers = new ArrayList<>(); - mediaItems = ImmutableList.of(); - } + private List mediaItems = ImmutableList.of(); public void setMediaItems(List mediaItems) { - this.mediaItems = ImmutableList.copyOf(mediaItems); + this.mediaItems = mediaItems; } + @Nullable private MediaSession session; + @Override public void onCreate() { super.onCreate(); Context context = ApplicationProvider.getApplicationContext(); ExoPlayer player = new TestExoPlayerBuilder(context).build(); - ForwardingPlayer forwardingPlayer = - new ForwardingPlayer(player) { - @Override - public void play() { - callers.add(session.getControllerForCurrentRequest()); - super.play(); - } - }; session = - new MediaSession.Builder(context, forwardingPlayer) + new MediaSession.Builder(context, player) .setCallback( new MediaSession.Callback() { @Override @@ -564,14 +546,11 @@ public class MediaSessionServiceTest { @Override public void onDestroy() { - if (session != null) { - session.getPlayer().stop(); - session.getPlayer().clearMediaItems(); - session.getPlayer().release(); - session.release(); - callers.clear(); - session = null; - } + session.getPlayer().stop(); + session.getPlayer().clearMediaItems(); + session.getPlayer().release(); + session.release(); + session = null; super.onDestroy(); } } diff --git a/libraries/test_session_current/build.gradle b/libraries/test_session_current/build.gradle index 43bdb61753..1d4a1bd5c1 100644 --- a/libraries/test_session_current/build.gradle +++ b/libraries/test_session_current/build.gradle @@ -41,10 +41,9 @@ android { dependencies { implementation project(modulePrefix + 'lib-session') implementation project(modulePrefix + 'test-session-common') - implementation project(modulePrefix + 'test-data') implementation 'androidx.media:media:' + androidxMediaVersion - implementation 'androidx.multidex:multidex:' + androidxMultidexVersion implementation 'androidx.test:core:' + androidxTestCoreVersion + implementation 'androidx.multidex:multidex:' + androidxMultidexVersion implementation project(modulePrefix + 'test-data') androidTestImplementation project(modulePrefix + 'lib-exoplayer') androidTestImplementation project(modulePrefix + 'test-utils') diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java index 192ea93ba6..7a013f98b9 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java @@ -26,7 +26,6 @@ import static androidx.media3.test.session.common.TestUtils.NO_RESPONSE_TIMEOUT_ import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS; import static com.google.common.truth.Truth.assertThat; import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static org.junit.Assert.fail; import android.content.Context; import android.os.Bundle; @@ -44,7 +43,6 @@ import androidx.media3.session.MediaSession.ControllerInfo; import androidx.media3.test.session.R; import androidx.media3.test.session.common.HandlerThreadTestRule; import androidx.media3.test.session.common.MainLooperTestRule; -import androidx.media3.test.session.common.TestHandler; import androidx.media3.test.session.common.TestUtils; import androidx.media3.test.utils.TestExoPlayerBuilder; import androidx.test.core.app.ApplicationProvider; @@ -63,6 +61,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; @@ -74,28 +73,16 @@ import org.junit.runner.RunWith; @LargeTest public class MediaSessionCallbackTest { - // Prepares the main looper. + private static final String TAG = "MSessionCallbackTest"; + @ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule(); - @Rule - public final HandlerThreadTestRule playerThreadTestRule = - new HandlerThreadTestRule("MSessionCallbackTest:player"); + @Rule public final HandlerThreadTestRule threadTestRule = new HandlerThreadTestRule(TAG); - @Rule - public final HandlerThreadTestRule controllerThreadTestRule = - new HandlerThreadTestRule("MSessionCallbackTest:controller"); + @Rule public final RemoteControllerTestRule controllerTestRule = new RemoteControllerTestRule(); @Rule public final MediaSessionTestRule sessionTestRule = new MediaSessionTestRule(); - // Used to create controllers in the service running in a different process. - @Rule - public final RemoteControllerTestRule remoteControllerTestRule = new RemoteControllerTestRule(); - - // Used to create controllers on a different thread in the local process. - @Rule - public final MediaControllerTestRule controllerTestRule = - new MediaControllerTestRule(controllerThreadTestRule); - private Context context; private MockPlayer player; private ListeningExecutorService executorService; @@ -105,7 +92,7 @@ public class MediaSessionCallbackTest { context = ApplicationProvider.getApplicationContext(); player = new MockPlayer.Builder() - .setApplicationLooper(playerThreadTestRule.getHandler().getLooper()) + .setApplicationLooper(threadTestRule.getHandler().getLooper()) .build(); // Intentionally use an Executor with another thread to test asynchronous workflows involving // background tasks. @@ -142,7 +129,7 @@ public class MediaSessionCallbackTest { .setId("testOnConnect_correctControllerVersions") .build()); - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(controllerVersion.get()).isEqualTo(MediaLibraryInfo.VERSION_INT); @@ -198,7 +185,7 @@ public class MediaSessionCallbackTest { "onConnect_acceptWithMissingSessionCommand_buttonDisabledAndPermissionDenied") .build()); RemoteMediaController remoteController = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); ImmutableList layout = remoteController.getCustomLayout(); @@ -228,7 +215,7 @@ public class MediaSessionCallbackTest { .setId("onConnect_emptyPlayerCommands_commandReleaseAlwaysIncluded") .build()); RemoteMediaController remoteController = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); assertThat(remoteController.getAvailableCommands().size()).isEqualTo(1); assertThat(remoteController.getAvailableCommands().contains(Player.COMMAND_RELEASE)).isTrue(); @@ -250,7 +237,7 @@ public class MediaSessionCallbackTest { .setCallback(callback) .setId("testOnPostConnect_afterConnected") .build()); - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); } @@ -276,7 +263,7 @@ public class MediaSessionCallbackTest { .setCallback(callback) .setId("testOnPostConnect_afterConnectionRejected") .build()); - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); assertThat(latch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse(); } @@ -309,7 +296,7 @@ public class MediaSessionCallbackTest { .setId("testOnCommandRequest") .build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); controller.prepare(); Thread.sleep(NO_RESPONSE_TIMEOUT_MS); @@ -371,7 +358,7 @@ public class MediaSessionCallbackTest { .setId("testOnCustomCommand") .build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); SessionResult result = controller.sendCustomCommand(testCommand, testArgs); assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); @@ -411,7 +398,7 @@ public class MediaSessionCallbackTest { .setId("testOnSetRating") .build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); SessionResult result = controller.setRating(testMediaId, testRating); assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); @@ -447,7 +434,7 @@ public class MediaSessionCallbackTest { .setId("testOnSetRating") .build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); SessionResult result = controller.setRating(testRating); assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); @@ -472,7 +459,7 @@ public class MediaSessionCallbackTest { sessionTestRule.ensureReleaseAfterTest( new MediaSession.Builder(context, player).setCallback(callback).build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); controller.setMediaItem(mediaItem); player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS); @@ -489,7 +476,7 @@ public class MediaSessionCallbackTest { MediaSession session = sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); // Default MediaSession.Callback.onAddMediaItems will be called controller.setMediaItemIncludeLocalConfiguration(mediaItemWithoutLocalConfiguration); @@ -511,7 +498,7 @@ public class MediaSessionCallbackTest { MediaSession session = sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); // Default MediaSession.Callback.onAddMediaItems will be called controller.setMediaItemsIncludeLocalConfiguration(mediaItemsWithoutLocalConfiguration); @@ -531,7 +518,7 @@ public class MediaSessionCallbackTest { MediaSession session = sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); // Default MediaSession.Callback.onAddMediaItems will be called controller.setMediaItemIncludeLocalConfiguration(mediaItemWithLocalConfiguration); @@ -551,7 +538,7 @@ public class MediaSessionCallbackTest { MediaSession session = sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); // Default MediaSession.Callback.onAddMediaItems will be called controller.setMediaItemsIncludeLocalConfiguration(fullMediaItems); @@ -578,7 +565,7 @@ public class MediaSessionCallbackTest { sessionTestRule.ensureReleaseAfterTest( new MediaSession.Builder(context, player).setCallback(callback).build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); controller.setMediaItem(mediaItem, /* startPositionMs= */ 1234); player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX, TIMEOUT_MS); @@ -607,7 +594,7 @@ public class MediaSessionCallbackTest { sessionTestRule.ensureReleaseAfterTest( new MediaSession.Builder(context, player).setCallback(callback).build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); controller.setMediaItem(mediaItem, /* resetPosition= */ true); player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS); @@ -635,7 +622,7 @@ public class MediaSessionCallbackTest { sessionTestRule.ensureReleaseAfterTest( new MediaSession.Builder(context, player).setCallback(callback).build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); controller.setMediaItems(mediaItems); player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS); @@ -664,7 +651,7 @@ public class MediaSessionCallbackTest { sessionTestRule.ensureReleaseAfterTest( new MediaSession.Builder(context, player).setCallback(callback).build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); controller.setMediaItems(mediaItems, /* startIndex= */ 1, /* startPositionMs= */ 1234); player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX, TIMEOUT_MS); @@ -695,7 +682,7 @@ public class MediaSessionCallbackTest { sessionTestRule.ensureReleaseAfterTest( new MediaSession.Builder(context, player).setCallback(callback).build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); controller.setMediaItems(mediaItems, /* resetPosition= */ true); player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS); @@ -725,7 +712,7 @@ public class MediaSessionCallbackTest { sessionTestRule.ensureReleaseAfterTest( new MediaSession.Builder(context, player).setCallback(callback).build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); controller.addMediaItem(mediaItem); player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS, TIMEOUT_MS); @@ -742,7 +729,7 @@ public class MediaSessionCallbackTest { MediaSession session = sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); // Default MediaSession.Callback.onAddMediaItems will be called controller.addMediaItemIncludeLocalConfiguration(mediaItemWithoutLocalConfiguration); @@ -763,7 +750,7 @@ public class MediaSessionCallbackTest { MediaSession session = sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); // Default MediaSession.Callback.onAddMediaItems will be called controller.addMediaItemsIncludeLocalConfiguration(mediaItemsWithoutLocalConfiguration); @@ -782,7 +769,7 @@ public class MediaSessionCallbackTest { MediaSession session = sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); // Default MediaSession.Callback.onAddMediaItems will be called controller.addMediaItemIncludeLocalConfiguration(mediaItemWithLocalConfiguration); @@ -802,7 +789,7 @@ public class MediaSessionCallbackTest { MediaSession session = sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); // Default MediaSession.Callback.onAddMediaItems will be called controller.addMediaItemsIncludeLocalConfiguration(fullMediaItems); @@ -830,7 +817,7 @@ public class MediaSessionCallbackTest { sessionTestRule.ensureReleaseAfterTest( new MediaSession.Builder(context, player).setCallback(callback).build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); controller.setMediaItem(existingItem); controller.addMediaItem(/* index= */ 1, mediaItem); @@ -862,7 +849,7 @@ public class MediaSessionCallbackTest { sessionTestRule.ensureReleaseAfterTest( new MediaSession.Builder(context, player).setCallback(callback).build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); controller.addMediaItems(mediaItems); player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS, TIMEOUT_MS); @@ -892,7 +879,7 @@ public class MediaSessionCallbackTest { sessionTestRule.ensureReleaseAfterTest( new MediaSession.Builder(context, player).setCallback(callback).build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); controller.setMediaItem(existingItem); controller.addMediaItems(/* index= */ 1, mediaItems); @@ -935,7 +922,7 @@ public class MediaSessionCallbackTest { sessionTestRule.ensureReleaseAfterTest( new MediaSession.Builder(context, player).setCallback(callback).build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); controller.setMediaItem(mediaItem, /* startPositionMs= */ 100); player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX, TIMEOUT_MS); @@ -973,7 +960,7 @@ public class MediaSessionCallbackTest { sessionTestRule.ensureReleaseAfterTest( new MediaSession.Builder(context, player).setCallback(callback).build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); controller.setMediaItems(mediaItems, /* startIndex= */ 1, /* startPositionMs= */ 100); player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX, TIMEOUT_MS); @@ -1013,7 +1000,7 @@ public class MediaSessionCallbackTest { sessionTestRule.ensureReleaseAfterTest( new MediaSession.Builder(context, player).setCallback(callback).build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); controller.setMediaItems(mediaItems, /* startIndex= */ 1, /* startPositionMs= */ 100); player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS); @@ -1052,7 +1039,7 @@ public class MediaSessionCallbackTest { sessionTestRule.ensureReleaseAfterTest( new MediaSession.Builder(context, player).setCallback(callback).build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); controller.setMediaItems(mediaItems, true); player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS); @@ -1090,7 +1077,7 @@ public class MediaSessionCallbackTest { sessionTestRule.ensureReleaseAfterTest( new MediaSession.Builder(context, player).setCallback(callback).build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); controller.play(); @@ -1111,7 +1098,7 @@ public class MediaSessionCallbackTest { MediaSession session = sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); controller.play(); @@ -1135,7 +1122,7 @@ public class MediaSessionCallbackTest { @Override public ListenableFuture onPlaybackResumption( MediaSession mediaSession, ControllerInfo controller) { - fail(); + Assert.fail(); return Futures.immediateFuture( new MediaSession.MediaItemsWithStartPosition( MediaTestUtils.createMediaItems(/* size= */ 10), @@ -1147,7 +1134,7 @@ public class MediaSessionCallbackTest { sessionTestRule.ensureReleaseAfterTest( new MediaSession.Builder(context, player).setCallback(callback).build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); controller.play(); @@ -1186,7 +1173,7 @@ public class MediaSessionCallbackTest { Bundle testConnectionHints = new Bundle(); testConnectionHints.putString("test_key", "test_value"); - remoteControllerTestRule.createRemoteController( + controllerTestRule.createRemoteController( session.getToken(), /* waitForConnection= */ false, testConnectionHints); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(TestUtils.equals(testConnectionHints, connectionHints.get())).isTrue(); @@ -1212,107 +1199,20 @@ public class MediaSessionCallbackTest { }) .build()); RemoteMediaController controller = - remoteControllerTestRule.createRemoteController(session.getToken()); + controllerTestRule.createRemoteController(session.getToken()); controller.release(); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); } @Test - public void - seekToNextMediaItem_controllerListenerTriggeredByMasking_commandNotYetArrivedAtSession() - throws Exception { - MediaItem mediaItem1 = - new MediaItem.Builder().setMediaId("id1").setUri("http://www.example.com/1").build(); - MediaItem mediaItem2 = - new MediaItem.Builder().setMediaId("id2").setUri("http://www.example.com/2").build(); - ExoPlayer testPlayer = - playerThreadTestRule - .getHandler() - .postAndSync( - () -> { - ExoPlayer exoPlayer = new TestExoPlayerBuilder(context).build(); - exoPlayer.setMediaItems(ImmutableList.of(mediaItem1, mediaItem2)); - return exoPlayer; - }); - List currentMediaItemsOfPlayer = new ArrayList<>(); - AtomicReference controller = new AtomicReference<>(); - List eventOrder = new ArrayList<>(); - CountDownLatch latch = new CountDownLatch(2); - // Listener added to player before the the session is built and the session adds a listener. - testPlayer.addListener( - new Player.Listener() { - @Override - public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) { - currentMediaItemsOfPlayer.add(testPlayer.getCurrentMediaItem()); - eventOrder.add("player.onMediaItemTransition"); - } - - @Override - public void onEvents(Player player, Player.Events events) { - if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION)) { - // Player still has the first item. Command has not yet arrived at the session. - currentMediaItemsOfPlayer.add(testPlayer.getCurrentMediaItem()); - eventOrder.add("player.onEvents"); - latch.countDown(); - } - } - }); - MediaSession session = - sessionTestRule.ensureReleaseAfterTest( - new MediaSession.Builder(context, testPlayer) - .setId( - "listener_controllerListenerTriggeredByMasking_commandNotYetArrivedAtSession") - .build()); - controller.set(controllerTestRule.createController(session.getToken())); - controller - .get() - .addListener( - /* listener= */ new Player.Listener() { - @Override - public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) { - eventOrder.add("controller.onMediaItemTransition"); - postToPlayerAndSync( - () -> currentMediaItemsOfPlayer.add(testPlayer.getCurrentMediaItem())); - } - - @Override - public void onEvents(Player player, Player.Events events) { - if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION)) { - // Triggered by masking in the same looper iteration as where - // controller.seekToNextMediaItem() is called. - eventOrder.add("controller.onEvents"); - postToPlayerAndSync( - () -> currentMediaItemsOfPlayer.add(testPlayer.getCurrentMediaItem())); - latch.countDown(); - } - } - }); - - postToControllerAndSync(controller.get()::seekToNextMediaItem); - - assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(currentMediaItemsOfPlayer) - .containsExactly(mediaItem1, mediaItem1, mediaItem2, mediaItem2) - .inOrder(); - assertThat(eventOrder) - .containsExactly( - "controller.onMediaItemTransition", - "controller.onEvents", - "player.onMediaItemTransition", - "player.onEvents") - .inOrder(); - postToControllerAndSync(() -> controller.get().release()); - } - - @Test - public void seekToNextMediaItem_playerListenerTriggeredByMasking_immediateCallHasStaleController() + public void seekToNextMediaItem_inProcessController_correctMediaItemTransitionsEvents() throws Exception { MediaItem mediaItem1 = new MediaItem.Builder().setMediaId("id1").setUri("http://www.example.com/1").build(); MediaItem mediaItem2 = new MediaItem.Builder().setMediaId("id2").setUri("http://www.example.com/2").build(); ExoPlayer testPlayer = - playerThreadTestRule + threadTestRule .getHandler() .postAndSync( () -> { @@ -1320,88 +1220,48 @@ public class MediaSessionCallbackTest { exoPlayer.setMediaItems(ImmutableList.of(mediaItem1, mediaItem2)); return exoPlayer; }); - List currentMediaIdsOfController = new ArrayList<>(); + List capturedMediaItemIds = new ArrayList<>(); + List capturedEvents = new ArrayList<>(); List eventOrder = new ArrayList<>(); - CountDownLatch latch = new CountDownLatch(2); - AtomicReference controller = new AtomicReference<>(); - // Listener added to player before the the session is built and the session adds a listener. - testPlayer.addListener( + CountDownLatch latch = new CountDownLatch(1); + MediaSession session = + sessionTestRule.ensureReleaseAfterTest( + new MediaSession.Builder(context, testPlayer) + .setId("seekToNextMediaItem_inProcessController_correctMediaItemTransitionsEvents") + .build()); + MediaController controller = + new MediaController.Builder(ApplicationProvider.getApplicationContext(), session.getToken()) + .setApplicationLooper(threadTestRule.getHandler().getLooper()) + .buildAsync() + .get(); + controller.addListener( new Player.Listener() { @Override public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) { - postToControllerAndSync( - () -> - currentMediaIdsOfController.add( - controller.get().getCurrentMediaItem().mediaId)); - eventOrder.add("player.onMediaItemTransition"); + capturedMediaItemIds.add(controller.getCurrentMediaItem().mediaId); + eventOrder.add("onMediaItemTransition"); } @Override public void onEvents(Player player, Player.Events events) { if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION)) { - postToControllerAndSync( - () -> - currentMediaIdsOfController.add( - controller.get().getCurrentMediaItem().mediaId)); - eventOrder.add("player.onEvents"); + capturedMediaItemIds.add(controller.getCurrentMediaItem().mediaId); + capturedEvents.add(events); + eventOrder.add("onEvents"); latch.countDown(); } } }); - MediaSession session = - sessionTestRule.ensureReleaseAfterTest( - new MediaSession.Builder(context, testPlayer) - .setId( - "listener_playerListenerTriggeredByMasking_statusUpdateArrivedAtSameProcessController") - .build()); - controller.set(controllerTestRule.createController(session.getToken())); - controller - .get() - .addListener( - new Player.Listener() { - @Override - public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) { - currentMediaIdsOfController.add(controller.get().getCurrentMediaItem().mediaId); - eventOrder.add("controller.onMediaItemTransition"); - } - @Override - public void onEvents(Player player, Player.Events events) { - if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION)) { - currentMediaIdsOfController.add(controller.get().getCurrentMediaItem().mediaId); - eventOrder.add("controller.onEvents"); - latch.countDown(); - } - } - }); - - postToPlayerAndSync(testPlayer::seekToNextMediaItem); + threadTestRule.getHandler().postAndSync(testPlayer::seekToNextMediaItem); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(currentMediaIdsOfController).containsExactly("id1", "id2", "id2", "id2").inOrder(); - assertThat(eventOrder) - .containsExactly( - "player.onMediaItemTransition", - "controller.onMediaItemTransition", - "controller.onEvents", - "player.onEvents") - .inOrder(); - } - - private void postToPlayerAndSync(TestHandler.TestRunnable r) { - try { - playerThreadTestRule.getHandler().postAndSync(r); - } catch (Exception e) { - fail(e.getMessage()); - } - } - - private void postToControllerAndSync(TestHandler.TestRunnable r) { - try { - controllerThreadTestRule.getHandler().postAndSync(r); - } catch (Exception e) { - fail(e.getMessage()); - } + assertThat(capturedMediaItemIds).containsExactly("id2", "id2").inOrder(); + assertThat(eventOrder).containsExactly("onMediaItemTransition", "onEvents").inOrder(); + assertThat(capturedEvents).hasSize(1); + assertThat(capturedEvents.get(0).size()).isEqualTo(2); + assertThat(capturedEvents.get(0).contains(Player.EVENT_MEDIA_ITEM_TRANSITION)).isTrue(); + assertThat(capturedEvents.get(0).contains(Player.EVENT_POSITION_DISCONTINUITY)).isTrue(); } private static MediaItem updateMediaItemWithLocalConfiguration(MediaItem mediaItem) { diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionTest.java index 71a355df51..b8f4ff04c4 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionTest.java @@ -15,13 +15,6 @@ */ package androidx.media3.session; -import static android.view.KeyEvent.KEYCODE_MEDIA_FAST_FORWARD; -import static android.view.KeyEvent.KEYCODE_MEDIA_NEXT; -import static android.view.KeyEvent.KEYCODE_MEDIA_PAUSE; -import static android.view.KeyEvent.KEYCODE_MEDIA_PLAY; -import static android.view.KeyEvent.KEYCODE_MEDIA_PREVIOUS; -import static android.view.KeyEvent.KEYCODE_MEDIA_REWIND; -import static android.view.KeyEvent.KEYCODE_MEDIA_STOP; import static androidx.media3.common.Player.STATE_IDLE; import static androidx.media3.test.session.common.TestUtils.LONG_TIMEOUT_MS; import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS; @@ -30,9 +23,7 @@ import static com.google.common.truth.Truth.assertWithMessage; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.junit.Assert.assertThrows; -import android.content.ComponentName; import android.content.Context; -import android.content.Intent; import android.os.Bundle; import android.os.HandlerThread; import android.os.Looper; @@ -40,14 +31,11 @@ import android.os.SystemClock; import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaSessionCompat; import android.text.TextUtils; -import android.view.KeyEvent; import androidx.media.MediaSessionManager; -import androidx.media3.common.ForwardingPlayer; import androidx.media3.common.MediaLibraryInfo; import androidx.media3.common.Player; import androidx.media3.common.util.Log; import androidx.media3.common.util.Util; -import androidx.media3.session.MediaSession.ControllerInfo; import androidx.media3.test.session.common.HandlerThreadTestRule; import androidx.media3.test.session.common.MainLooperTestRule; import androidx.media3.test.session.common.TestHandler; @@ -61,7 +49,6 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; import org.junit.After; import org.junit.Before; import org.junit.ClassRule; @@ -95,6 +82,7 @@ public class MediaSessionTest { context = ApplicationProvider.getApplicationContext(); handler = threadTestRule.getHandler(); player = new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build(); + session = sessionTestRule.ensureReleaseAfterTest( new MediaSession.Builder(context, player) @@ -103,7 +91,7 @@ public class MediaSessionTest { new MediaSession.Callback() { @Override public MediaSession.ConnectionResult onConnect( - MediaSession session, ControllerInfo controller) { + MediaSession session, MediaSession.ControllerInfo controller) { if (TextUtils.equals( context.getPackageName(), controller.getPackageName())) { return MediaSession.Callback.super.onConnect(session, controller); @@ -161,8 +149,7 @@ public class MediaSessionTest { // expected. pass-through } // Empty string as ID is allowed. - sessionTestRule.ensureReleaseAfterTest( - new MediaSession.Builder(context, player).setId("").build()); + new MediaSession.Builder(context, player).setId("").build().release(); } @Test @@ -341,7 +328,7 @@ public class MediaSessionTest { new MediaSession.Callback() { @Override public MediaSession.ConnectionResult onConnect( - MediaSession session, ControllerInfo controller) { + MediaSession session, MediaSession.ControllerInfo controller) { Future result = session.sendCustomCommand(controller, testCommand, /* args= */ Bundle.EMPTY); try { @@ -355,7 +342,7 @@ public class MediaSessionTest { } @Override - public void onPostConnect(MediaSession session, ControllerInfo controller) { + public void onPostConnect(MediaSession session, MediaSession.ControllerInfo controller) { Future result = session.sendCustomCommand(controller, testCommand, /* args= */ Bundle.EMPTY); try { @@ -378,6 +365,10 @@ public class MediaSessionTest { /** Test {@link MediaSession#getSessionCompatToken()}. */ @Test public void getSessionCompatToken_returnsCompatibleWithMediaControllerCompat() throws Exception { + String expectedControllerCompatPackageName = + (21 <= Util.SDK_INT && Util.SDK_INT < 24) + ? MediaSessionManager.RemoteUserInfo.LEGACY_CONTROLLER + : context.getPackageName(); MediaSession session = sessionTestRule.ensureReleaseAfterTest( new MediaSession.Builder(context, player) @@ -386,10 +377,9 @@ public class MediaSessionTest { new MediaSession.Callback() { @Override public MediaSession.ConnectionResult onConnect( - MediaSession session, ControllerInfo controller) { + MediaSession session, MediaSession.ControllerInfo controller) { if (TextUtils.equals( - getControllerCallerPackageName(controller), - controller.getPackageName())) { + expectedControllerCompatPackageName, controller.getPackageName())) { return MediaSession.Callback.super.onConnect(session, controller); } return MediaSession.ConnectionResult.reject(); @@ -426,7 +416,7 @@ public class MediaSessionTest { new MediaSession.Callback() { @Override public MediaSession.ConnectionResult onConnect( - MediaSession session, ControllerInfo controller) { + MediaSession session, MediaSession.ControllerInfo controller) { controllerVersionRef.set(controller.getControllerVersion()); connectedLatch.countDown(); return MediaSession.Callback.super.onConnect(session, controller); @@ -504,302 +494,4 @@ public class MediaSessionTest { assertThat(bufferedPositionsMs).containsExactly(0L, 0L, 0L, 0L, 0L).inOrder(); } - - @Test - public void onMediaButtonEvent_allSupportedKeys_notificationControllerConnected_dispatchesEvent() - throws Exception { - AtomicReference session = new AtomicReference<>(); - CallerCollectorPlayer callerCollectorPlayer = new CallerCollectorPlayer(player, session); - session.set( - sessionTestRule.ensureReleaseAfterTest( - new MediaSession.Builder(context, callerCollectorPlayer) - .setId("getSessionCompatToken_returnsCompatibleWithMediaControllerCompat") - .setCallback( - new MediaSession.Callback() { - @Override - public MediaSession.ConnectionResult onConnect( - MediaSession session, ControllerInfo controller) { - if (TextUtils.equals( - context.getPackageName(), controller.getPackageName())) { - return MediaSession.Callback.super.onConnect(session, controller); - } - return MediaSession.ConnectionResult.reject(); - } - }) - .build())); - Bundle connectionHints = new Bundle(); - connectionHints.putBoolean(MediaNotificationManager.KEY_MEDIA_NOTIFICATION_MANAGER, true); - new MediaController.Builder( - ApplicationProvider.getApplicationContext(), session.get().getToken()) - .setConnectionHints(connectionHints) - .buildAsync() - .get(); - - MediaSessionImpl impl = session.get().getImpl(); - assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_PLAY))).isTrue(); - assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_PAUSE))).isTrue(); - assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_FAST_FORWARD))).isTrue(); - assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_REWIND))).isTrue(); - assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_NEXT))).isTrue(); - assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_PREVIOUS))).isTrue(); - assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_STOP))).isTrue(); - - player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); - player.awaitMethodCalled(MockPlayer.METHOD_PAUSE, TIMEOUT_MS); - player.awaitMethodCalled(MockPlayer.METHOD_SEEK_FORWARD, TIMEOUT_MS); - player.awaitMethodCalled(MockPlayer.METHOD_SEEK_FORWARD, TIMEOUT_MS); - player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_NEXT, TIMEOUT_MS); - player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_PREVIOUS, TIMEOUT_MS); - player.awaitMethodCalled(MockPlayer.METHOD_STOP, TIMEOUT_MS); - assertThat(callerCollectorPlayer.callingControllers).hasSize(7); - for (ControllerInfo controllerInfo : callerCollectorPlayer.callingControllers) { - assertThat(session.get().isMediaNotificationController(controllerInfo)).isTrue(); - } - } - - @Test - public void - onMediaButtonEvent_allSupportedKeys_notificationControllerNotConnected_dispatchesEventThroughFrameworkFallback() - throws Exception { - AtomicReference session = new AtomicReference<>(); - CallerCollectorPlayer callerCollectorPlayer = new CallerCollectorPlayer(player, session); - session.set( - sessionTestRule.ensureReleaseAfterTest( - new MediaSession.Builder(context, callerCollectorPlayer) - .setId("getSessionCompatToken_returnsCompatibleWithMediaControllerCompat") - .setCallback( - new MediaSession.Callback() { - @Override - public MediaSession.ConnectionResult onConnect( - MediaSession session, ControllerInfo controller) { - if (TextUtils.equals( - getControllerCallerPackageName(controller), - controller.getPackageName())) { - return MediaSession.Callback.super.onConnect(session, controller); - } - return MediaSession.ConnectionResult.reject(); - } - }) - .build())); - MediaSessionImpl impl = session.get().getImpl(); - - assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_PLAY))).isTrue(); - assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_PAUSE))).isTrue(); - assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_FAST_FORWARD))).isTrue(); - assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_REWIND))).isTrue(); - assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_NEXT))).isTrue(); - assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_PREVIOUS))).isTrue(); - assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_STOP))).isTrue(); - - // Fallback code path through platform session when MediaSessionImpl doesn't handle the event. - player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); - player.awaitMethodCalled(MockPlayer.METHOD_PAUSE, TIMEOUT_MS); - player.awaitMethodCalled(MockPlayer.METHOD_SEEK_FORWARD, TIMEOUT_MS); - player.awaitMethodCalled(MockPlayer.METHOD_SEEK_FORWARD, TIMEOUT_MS); - player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_NEXT, TIMEOUT_MS); - player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_PREVIOUS, TIMEOUT_MS); - player.awaitMethodCalled(MockPlayer.METHOD_STOP, TIMEOUT_MS); - assertThat(callerCollectorPlayer.callingControllers).hasSize(7); - for (ControllerInfo controllerInfo : callerCollectorPlayer.callingControllers) { - assertThat(session.get().isMediaNotificationController(controllerInfo)).isFalse(); - assertThat(controllerInfo.getControllerVersion()) - .isEqualTo(ControllerInfo.LEGACY_CONTROLLER_VERSION); - assertThat(controllerInfo.getPackageName()) - .isEqualTo(getControllerCallerPackageName(controllerInfo)); - } - } - - @Test - public void onMediaButtonEvent_noKeyEvent_returnsFalse() { - Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY); - intent.removeExtra(Intent.EXTRA_KEY_EVENT); - - boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent); - - assertThat(isEventHandled).isFalse(); - } - - @Test - public void onMediaButtonEvent_noKeyEvent_mediaNotificationControllerConnected_returnsFalse() - throws Exception { - Bundle connectionHints = new Bundle(); - connectionHints.putBoolean(MediaNotificationManager.KEY_MEDIA_NOTIFICATION_MANAGER, true); - new MediaController.Builder(ApplicationProvider.getApplicationContext(), session.getToken()) - .setConnectionHints(connectionHints) - .buildAsync() - .get(); - Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY); - intent.removeExtra(Intent.EXTRA_KEY_EVENT); - - boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent); - - assertThat(isEventHandled).isFalse(); - } - - @Test - public void onMediaButtonEvent_invalidKeyEvent_returnsFalse() { - Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY); - intent.removeExtra(Intent.EXTRA_KEY_EVENT); - intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KEYCODE_MEDIA_PAUSE)); - - boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent); - - assertThat(isEventHandled).isFalse(); - } - - @Test - public void onMediaButtonEvent_invalidKeyEvent_mediaNotificationControllerConnected_returnsFalse() - throws Exception { - Bundle connectionHints = new Bundle(); - connectionHints.putBoolean(MediaNotificationManager.KEY_MEDIA_NOTIFICATION_MANAGER, true); - new MediaController.Builder(ApplicationProvider.getApplicationContext(), session.getToken()) - .setConnectionHints(connectionHints) - .buildAsync() - .get(); - Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY); - intent.removeExtra(Intent.EXTRA_KEY_EVENT); - intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KEYCODE_MEDIA_PAUSE)); - - boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent); - - assertThat(isEventHandled).isFalse(); - } - - @Test - public void onMediaButtonEvent_invalidAction_returnsFalse() { - Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY); - intent.setAction("notAMediaButtonAction"); - - boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent); - - assertThat(isEventHandled).isFalse(); - } - - @Test - public void onMediaButtonEvent_invalidAction_mediaNotificationControllerConnected_returnsFalse() - throws Exception { - Bundle connectionHints = new Bundle(); - connectionHints.putBoolean(MediaNotificationManager.KEY_MEDIA_NOTIFICATION_MANAGER, true); - new MediaController.Builder(ApplicationProvider.getApplicationContext(), session.getToken()) - .setConnectionHints(connectionHints) - .buildAsync() - .get(); - Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY); - intent.setAction("notAMediaButtonAction"); - - boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent); - - assertThat(isEventHandled).isFalse(); - } - - @Test - public void onMediaButtonEvent_invalidComponent_returnsFalse() { - Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY); - intent.setComponent(new ComponentName("a.package", "a.class")); - - boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent); - - assertThat(isEventHandled).isFalse(); - } - - @Test - public void - onMediaButtonEvent_invalidComponent_mediaNotificationControllerConnected_returnsFalse() - throws Exception { - Bundle connectionHints = new Bundle(); - connectionHints.putBoolean(MediaNotificationManager.KEY_MEDIA_NOTIFICATION_MANAGER, true); - new MediaController.Builder(ApplicationProvider.getApplicationContext(), session.getToken()) - .setConnectionHints(connectionHints) - .buildAsync() - .get(); - Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY); - intent.setComponent(new ComponentName("a.package", "a.class")); - - boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent); - - assertThat(isEventHandled).isFalse(); - } - - private static Intent getMediaButtonIntent(int keyCode) { - Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); - intent.setComponent( - new ComponentName(ApplicationProvider.getApplicationContext(), Object.class)); - intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); - return intent; - } - - /** - * Returns the expected {@link MediaSessionManager.RemoteUserInfo#getPackageName()} of a - * controller hosted in the test companion app. - * - *

Before API 21 and after API 23 the package name is {@link Context#getPackageName()} of the - * {@link ApplicationProvider#getApplicationContext() application under test}. - * - *

The early implementations (API 21 - 23), the platform MediaSession doesn't report the caller - * package name. Instead the package of the RemoteUserInfo is set for all external controllers to - * the same {@code MediaSessionManager.RemoteUserInfo.LEGACY_CONTROLLER} (see - * MediaSessionCompat.MediaSessionCallbackApi21.setCurrentControllerInfo()). - * - *

Calling this method should only be required to test legacy behaviour. - */ - private static String getControllerCallerPackageName(ControllerInfo controllerInfo) { - return (Util.SDK_INT < 21 - || Util.SDK_INT > 23 - || controllerInfo.getControllerVersion() != ControllerInfo.LEGACY_CONTROLLER_VERSION) - ? ApplicationProvider.getApplicationContext().getPackageName() - : MediaSessionManager.RemoteUserInfo.LEGACY_CONTROLLER; - } - - private static class CallerCollectorPlayer extends ForwardingPlayer { - private final List callingControllers; - private final AtomicReference session; - - public CallerCollectorPlayer(Player player, AtomicReference mediaSession) { - super(player); - this.session = mediaSession; - callingControllers = new ArrayList<>(); - } - - @Override - public void play() { - callingControllers.add(session.get().getControllerForCurrentRequest()); - super.play(); - } - - @Override - public void pause() { - callingControllers.add(session.get().getControllerForCurrentRequest()); - super.pause(); - } - - @Override - public void seekBack() { - callingControllers.add(session.get().getControllerForCurrentRequest()); - super.seekBack(); - } - - @Override - public void seekForward() { - callingControllers.add(session.get().getControllerForCurrentRequest()); - super.seekForward(); - } - - @Override - public void seekToNext() { - callingControllers.add(session.get().getControllerForCurrentRequest()); - super.seekToNext(); - } - - @Override - public void seekToPrevious() { - callingControllers.add(session.get().getControllerForCurrentRequest()); - super.seekToPrevious(); - } - - @Override - public void stop() { - callingControllers.add(session.get().getControllerForCurrentRequest()); - super.stop(); - } - } }