diff --git a/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaSessionConstants.java b/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaSessionConstants.java index 2e4040f2d9..4791aad8fc 100644 --- a/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaSessionConstants.java +++ b/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaSessionConstants.java @@ -23,6 +23,7 @@ public class MediaSessionConstants { // Test method names public static final String TEST_GET_SESSION_ACTIVITY = "testGetSessionActivity"; + public static final String TEST_WITH_CUSTOM_COMMANDS = "testWithCustomCommands"; public static final String TEST_CONTROLLER_LISTENER_SESSION_REJECTS = "connection_sessionRejects"; public static final String TEST_IS_SESSION_COMMAND_AVAILABLE = "testIsSessionCommandAvailable"; diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionCompatTest.java new file mode 100644 index 0000000000..29e36e682d --- /dev/null +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionCompatTest.java @@ -0,0 +1,140 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.session; + +import static androidx.media3.test.session.common.CommonConstants.DEFAULT_TEST_NAME; +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.Context; +import android.os.Bundle; +import android.os.RemoteException; +import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import androidx.media3.test.session.common.HandlerThreadTestRule; +import androidx.media3.test.session.common.MainLooperTestRule; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; + +/** + * Tests for {@link MediaControllerCompat.Callback} with {@link MediaSessionCompat}. + * + *

The tests in this class represents the reference specific usages of the legacy API for which + * we need to provide support with the Media3 API. + * + *

So these test are actually not tests but rather a reference of how the legacy API is used and + * expected to work. + */ +@RunWith(AndroidJUnit4.class) +@LargeTest +public class MediaControllerCompatCallbackWithMediaSessionCompatTest { + + private static final int TIMEOUT_MS = 1_000; + + @ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule(); + + private final HandlerThreadTestRule threadTestRule = + new HandlerThreadTestRule("MediaControllerCompatCallbackWithMediaSessionCompatTest"); + private final MediaControllerTestRule controllerTestRule = + new MediaControllerTestRule(threadTestRule); + + @Rule + public final TestRule chain = RuleChain.outerRule(threadTestRule).around(controllerTestRule); + + private Context context; + private RemoteMediaSessionCompat session; + + @Before + public void setUp() throws Exception { + context = ApplicationProvider.getApplicationContext(); + session = new RemoteMediaSessionCompat(DEFAULT_TEST_NAME, context); + } + + @After + public void cleanUp() throws RemoteException { + session.cleanUp(); + } + + /** Custom actions in the legacy session used for instance by Android Auto and Wear OS. */ + @Test + public void setPlaybackState_withCustomActions_onPlaybackStateCompatChangedCalled() + throws Exception { + MediaSessionCompat.Token sessionToken = session.getSessionToken(); + Bundle extras1 = new Bundle(); + extras1.putString("key", "value-1"); + PlaybackStateCompat.CustomAction customAction1 = + new PlaybackStateCompat.CustomAction.Builder("action1", "actionName1", /* icon= */ 1) + .setExtras(extras1) + .build(); + Bundle extras2 = new Bundle(); + extras2.putString("key", "value-2"); + PlaybackStateCompat.CustomAction customAction2 = + new PlaybackStateCompat.CustomAction.Builder("action2", "actionName2", /* icon= */ 2) + .setExtras(extras2) + .build(); + PlaybackStateCompat.Builder builder = + new PlaybackStateCompat.Builder() + .addCustomAction(customAction1) + .addCustomAction(customAction2); + List receivedActions = new ArrayList<>(); + List receivedDisplayNames = new ArrayList<>(); + List receivedBundleValues = new ArrayList<>(); + List receivedIconResIds = new ArrayList<>(); + CountDownLatch countDownLatch = new CountDownLatch(1); + threadTestRule + .getHandler() + .postAndSync( + () -> { + MediaControllerCompat mediaControllerCompat = + new MediaControllerCompat(context, sessionToken); + mediaControllerCompat.registerCallback( + new MediaControllerCompat.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackStateCompat state) { + List layout = state.getCustomActions(); + for (PlaybackStateCompat.CustomAction action : layout) { + receivedActions.add(action.getAction()); + receivedDisplayNames.add(String.valueOf(action.getName())); + receivedBundleValues.add(action.getExtras().getString("key")); + receivedIconResIds.add(action.getIcon()); + } + countDownLatch.countDown(); + } + }); + }); + + session.setPlaybackState(builder.build()); + + assertThat(countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(receivedActions).containsExactly("action1", "action2").inOrder(); + assertThat(receivedDisplayNames).containsExactly("actionName1", "actionName2").inOrder(); + assertThat(receivedIconResIds).containsExactly(1, 2).inOrder(); + assertThat(receivedBundleValues).containsExactly("value-1", "value-2").inOrder(); + } +} diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java index 2a7af750fd..d025108b07 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java @@ -769,52 +769,54 @@ public class MediaControllerCompatCallbackWithMediaSessionTest { } @Test - public void customLayoutChanged_updatesPlaybackStateCompat() throws Exception { - - AtomicReference playbackStateRef = new AtomicReference<>(); + public void setCustomLayout_onPlaybackStateCompatChangedCalled() throws Exception { + List buttons = new ArrayList<>(); + Bundle extras1 = new Bundle(); + extras1.putString("key", "value-1"); + CommandButton button1 = + new CommandButton.Builder() + .setSessionCommand(new SessionCommand("action1", extras1)) + .setDisplayName("actionName1") + .setIconResId(1) + .build(); + Bundle extras2 = new Bundle(); + extras2.putString("key", "value-2"); + CommandButton button2 = + new CommandButton.Builder() + .setSessionCommand(new SessionCommand("action2", extras2)) + .setDisplayName("actionName2") + .setIconResId(2) + .build(); + buttons.add(button1); + buttons.add(button2); + List receivedActions = new ArrayList<>(); + List receivedDisplayNames = new ArrayList<>(); + List receivedBundleValues = new ArrayList<>(); + List receivedIconResIds = new ArrayList<>(); CountDownLatch latch = new CountDownLatch(1); MediaControllerCompat.Callback callback = new MediaControllerCompat.Callback() { @Override public void onPlaybackStateChanged(PlaybackStateCompat state) { - playbackStateRef.set(state); + List layout = state.getCustomActions(); + for (PlaybackStateCompat.CustomAction action : layout) { + receivedActions.add(action.getAction()); + receivedDisplayNames.add(String.valueOf(action.getName())); + receivedBundleValues.add(action.getExtras().getString("key")); + receivedIconResIds.add(action.getIcon()); + } latch.countDown(); } }; controllerCompat.registerCallback(callback, handler); - List customLayout = new ArrayList<>(); - Bundle customCommandBundle1 = new Bundle(); - customCommandBundle1.putString("customKey1", "customValue1"); - customLayout.add( - new CommandButton.Builder() - .setDisplayName("customCommandName1") - .setIconResId(1) - .setSessionCommand(new SessionCommand("customCommandAction1", customCommandBundle1)) - .build()); - Bundle customCommandBundle2 = new Bundle(); - customCommandBundle2.putString("customKey2", "customValue2"); - customLayout.add( - new CommandButton.Builder() - .setDisplayName("customCommandName2") - .setIconResId(2) - .setSessionCommand(new SessionCommand("customCommandAction2", customCommandBundle2)) - .build()); - - session.setCustomLayout(customLayout); + session.setCustomLayout(buttons); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - List customActions = - playbackStateRef.get().getCustomActions(); - assertThat(customActions).hasSize(2); - assertThat(customActions.get(0).getAction()).isEqualTo("customCommandAction1"); - assertThat(customActions.get(0).getName()).isEqualTo("customCommandName1"); - assertThat(customActions.get(0).getIcon()).isEqualTo(1); - assertThat(TestUtils.equals(customActions.get(0).getExtras(), customCommandBundle1)).isTrue(); - assertThat(customActions.get(1).getAction()).isEqualTo("customCommandAction2"); - assertThat(customActions.get(1).getName()).isEqualTo("customCommandName2"); - assertThat(customActions.get(1).getIcon()).isEqualTo(2); - assertThat(TestUtils.equals(customActions.get(1).getExtras(), customCommandBundle2)).isTrue(); + assertThat(receivedActions).containsExactly("action1", "action2").inOrder(); + assertThat(receivedDisplayNames).containsExactly("actionName1", "actionName2").inOrder(); + assertThat(receivedIconResIds).containsExactly(1, 2).inOrder(); + assertThat(receivedBundleValues).containsExactly("value-1", "value-2").inOrder(); } @Test diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java index ebc4315467..378506b08e 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java @@ -15,7 +15,6 @@ */ package androidx.media3.session; -import static androidx.media3.common.Player.COMMAND_PLAY_PAUSE; import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE; import static androidx.media3.common.Player.EVENT_REPEAT_MODE_CHANGED; import static androidx.media3.common.Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED; @@ -30,6 +29,7 @@ import static androidx.media3.test.session.common.CommonConstants.DEFAULT_TEST_N import static androidx.media3.test.session.common.CommonConstants.MOCK_MEDIA3_LIBRARY_SERVICE; import static androidx.media3.test.session.common.CommonConstants.MOCK_MEDIA3_SESSION_SERVICE; import static androidx.media3.test.session.common.MediaSessionConstants.TEST_CONTROLLER_LISTENER_SESSION_REJECTS; +import static androidx.media3.test.session.common.MediaSessionConstants.TEST_WITH_CUSTOM_COMMANDS; import static androidx.media3.test.session.common.TestUtils.LONG_TIMEOUT_MS; import static androidx.media3.test.session.common.TestUtils.NO_RESPONSE_TIMEOUT_MS; import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS; @@ -1743,37 +1743,61 @@ public class MediaControllerListenerTest { } @Test - public void onCustomLayoutChanged() throws Exception { + public void setCustomLayout_onSetCustomLayoutCalled() throws Exception { List buttons = new ArrayList<>(); - - CommandButton button = + Bundle extras1 = new Bundle(); + extras1.putString("key", "value-1"); + CommandButton button1 = new CommandButton.Builder() - .setPlayerCommand(COMMAND_PLAY_PAUSE) - .setDisplayName("button") + .setSessionCommand(new SessionCommand("action1", extras1)) + .setDisplayName("actionName1") + .setIconResId(1) .build(); - buttons.add(button); - + Bundle extras2 = new Bundle(); + extras2.putString("key", "value-2"); + CommandButton button2 = + new CommandButton.Builder() + .setSessionCommand(new SessionCommand("action2", extras2)) + .setDisplayName("actionName2") + .setIconResId(2) + .build(); + buttons.add(button1); + buttons.add(button2); CountDownLatch latch = new CountDownLatch(1); + List receivedActions = new ArrayList<>(); + List receivedDisplayNames = new ArrayList<>(); + List receivedBundleValues = new ArrayList<>(); + List receivedIconResIds = new ArrayList<>(); + List receivedCommandCodes = new ArrayList<>(); MediaController.Listener listener = new MediaController.Listener() { @Override public ListenableFuture onSetCustomLayout( MediaController controller, List layout) { - assertThat(layout).hasSize(buttons.size()); - for (int i = 0; i < layout.size(); i++) { - assertThat(layout.get(i).playerCommand).isEqualTo(buttons.get(i).playerCommand); - assertThat(layout.get(i).displayName.toString()) - .isEqualTo(buttons.get(i).displayName.toString()); + for (CommandButton button : layout) { + receivedActions.add(button.sessionCommand.customAction); + receivedDisplayNames.add(String.valueOf(button.displayName)); + receivedBundleValues.add(button.sessionCommand.customExtras.getString("key")); + receivedCommandCodes.add(button.sessionCommand.commandCode); + receivedIconResIds.add(button.iconResId); } latch.countDown(); return Futures.immediateFuture(new SessionResult(RESULT_SUCCESS)); } }; - controllerTestRule.createController( - remoteSession.getToken(), /* connectionHints= */ null, listener); + RemoteMediaSession session = createRemoteMediaSession(TEST_WITH_CUSTOM_COMMANDS); + controllerTestRule.createController(session.getToken(), /* connectionHints= */ null, listener); + + session.setCustomLayout(buttons); - remoteSession.setCustomLayout(buttons); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(receivedActions).containsExactly("action1", "action2").inOrder(); + assertThat(receivedCommandCodes) + .containsExactly(SessionCommand.COMMAND_CODE_CUSTOM, SessionCommand.COMMAND_CODE_CUSTOM) + .inOrder(); + assertThat(receivedDisplayNames).containsExactly("actionName1", "actionName2").inOrder(); + assertThat(receivedIconResIds).containsExactly(1, 2).inOrder(); + assertThat(receivedBundleValues).containsExactly("value-1", "value-2").inOrder(); } @Test diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java index c22064fdc0..67a02b3b44 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java @@ -16,12 +16,14 @@ package androidx.media3.session; import static androidx.media3.common.Player.EVENT_REPEAT_MODE_CHANGED; +import static androidx.media3.session.SessionResult.RESULT_SUCCESS; import static androidx.media3.test.session.common.CommonConstants.DEFAULT_TEST_NAME; 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 android.content.Context; +import android.os.Bundle; import android.os.RemoteException; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; @@ -33,6 +35,10 @@ import androidx.media3.test.session.common.MainLooperTestRule; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; +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.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; @@ -108,4 +114,58 @@ public class MediaControllerListenerWithMediaSessionCompatTest { assertThat(listenerEventCodes).containsExactly(EVENT_REPEAT_MODE_CHANGED, EVENT_ON_EVENTS); assertThat(eventsRef.get()).isEqualTo(testEvents); } + + @Test + public void setPlaybackState_withCustomActions_onSetCustomLayoutCalled() throws Exception { + Bundle extras1 = new Bundle(); + extras1.putString("key", "value-1"); + PlaybackStateCompat.CustomAction customAction1 = + new PlaybackStateCompat.CustomAction.Builder("action1", "actionName1", /* icon= */ 1) + .setExtras(extras1) + .build(); + Bundle extras2 = new Bundle(); + extras2.putString("key", "value-2"); + PlaybackStateCompat.CustomAction customAction2 = + new PlaybackStateCompat.CustomAction.Builder("action2", "actionName2", /* icon= */ 2) + .setExtras(extras2) + .build(); + PlaybackStateCompat.Builder builder = + new PlaybackStateCompat.Builder() + .addCustomAction(customAction1) + .addCustomAction(customAction2); + List receivedActions = new ArrayList<>(); + List receivedDisplayNames = new ArrayList<>(); + List receivedBundleValues = new ArrayList<>(); + List receivedIconResIds = new ArrayList<>(); + List receivedCommandCodes = new ArrayList<>(); + CountDownLatch countDownLatch = new CountDownLatch(1); + controllerTestRule.createController( + session.getSessionToken(), + new MediaController.Listener() { + @Override + public ListenableFuture onSetCustomLayout( + MediaController controller, List layout) { + for (CommandButton button : layout) { + receivedActions.add(button.sessionCommand.customAction); + receivedDisplayNames.add(String.valueOf(button.displayName)); + receivedBundleValues.add(button.sessionCommand.customExtras.getString("key")); + receivedCommandCodes.add(button.sessionCommand.commandCode); + receivedIconResIds.add(button.iconResId); + } + countDownLatch.countDown(); + return Futures.immediateFuture(new SessionResult(RESULT_SUCCESS)); + } + }); + + session.setPlaybackState(builder.build()); + + assertThat(countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(receivedActions).containsExactly("action1", "action2").inOrder(); + assertThat(receivedCommandCodes) + .containsExactly(SessionCommand.COMMAND_CODE_CUSTOM, SessionCommand.COMMAND_CODE_CUSTOM) + .inOrder(); + assertThat(receivedDisplayNames).containsExactly("actionName1", "actionName2").inOrder(); + assertThat(receivedIconResIds).containsExactly(1, 2).inOrder(); + assertThat(receivedBundleValues).containsExactly("value-1", "value-2").inOrder(); + } } diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java index 25202ac0ac..3b3b06e3bb 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java @@ -57,6 +57,7 @@ import static androidx.media3.test.session.common.MediaSessionConstants.KEY_AVAI import static androidx.media3.test.session.common.MediaSessionConstants.TEST_CONTROLLER_LISTENER_SESSION_REJECTS; import static androidx.media3.test.session.common.MediaSessionConstants.TEST_GET_SESSION_ACTIVITY; import static androidx.media3.test.session.common.MediaSessionConstants.TEST_IS_SESSION_COMMAND_AVAILABLE; +import static androidx.media3.test.session.common.MediaSessionConstants.TEST_WITH_CUSTOM_COMMANDS; import android.app.PendingIntent; import android.app.Service; @@ -177,6 +178,24 @@ public class MediaSessionProviderService extends Service { builder.setSessionActivity(pendingIntent); break; } + case TEST_WITH_CUSTOM_COMMANDS: + { + SessionCommands availableSessionCommands = + new SessionCommands.Builder() + .add(new SessionCommand("action1", Bundle.EMPTY)) + .add(new SessionCommand("action2", Bundle.EMPTY)) + .build(); + builder.setCallback( + new MediaSession.Callback() { + @Override + public MediaSession.ConnectionResult onConnect( + MediaSession session, ControllerInfo controller) { + return MediaSession.ConnectionResult.accept( + availableSessionCommands, Player.Commands.EMPTY); + } + }); + break; + } case TEST_CONTROLLER_LISTENER_SESSION_REJECTS: { builder.setCallback(