diff --git a/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java b/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java index 84faddd1ce..6752a386c3 100644 --- a/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java +++ b/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java @@ -121,7 +121,9 @@ import java.lang.annotation.Target; || !playerCommandsFromSession.contains(Player.COMMAND_GET_MEDIA_ITEMS_METADATA), /* excludeCues= */ !playerCommandsFromPlayer.contains(Player.COMMAND_GET_TEXT) || !playerCommandsFromSession.contains(Player.COMMAND_GET_TEXT), - /* excludeTimeline= */ false)); + /* excludeTimeline= */ false, + /* excludeTracks= */ !playerCommandsFromPlayer.contains(Player.COMMAND_GET_TRACKS) + || !playerCommandsFromSession.contains(Player.COMMAND_GET_TRACKS))); return bundle; } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java index 4434e2f2b4..e27d40cd5c 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java @@ -2395,9 +2395,9 @@ import org.checkerframework.checker.nullness.qual.NonNull; listener.onMediaItemTransition( currentMediaItem, playerInfo.mediaItemTransitionReason)); } - if (!Util.areEqual(oldPlayerInfo.currentTracks, newPlayerInfo.currentTracks)) { + if (!Util.areEqual(oldPlayerInfo.currentTracks, playerInfo.currentTracks)) { listeners.queueEvent( - EVENT_TRACKS_CHANGED, listener -> listener.onTracksChanged(newPlayerInfo.currentTracks)); + EVENT_TRACKS_CHANGED, listener -> listener.onTracksChanged(playerInfo.currentTracks)); } if (!Util.areEqual(oldPlayerInfo.playbackParameters, playerInfo.playbackParameters)) { listeners.queueEvent( 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 12e188ef09..ddca5663bf 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -1070,7 +1070,8 @@ public class MediaSession { boolean excludeMediaItems, boolean excludeMediaItemsMetadata, boolean excludeCues, - boolean excludeTimeline) + boolean excludeTimeline, + boolean excludeTracks) throws RemoteException {} default void onPeriodicSessionPositionInfoChanged( 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 de75f0bd72..d147f47791 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -411,7 +411,10 @@ import org.checkerframework.checker.initialization.qual.Initialized; /* excludeCues= */ !sessionStub .getConnectedControllersManager() .isPlayerCommandAvailable(controller, Player.COMMAND_GET_TEXT), - excludeTimeline); + excludeTimeline, + /* excludeTracks= */ !sessionStub + .getConnectedControllersManager() + .isPlayerCommandAvailable(controller, Player.COMMAND_GET_TRACKS)); } catch (DeadObjectException e) { onDeadObjectException(controller); } catch (RemoteException e) { 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 d1606270c7..56bb1a5f62 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -1710,12 +1710,17 @@ import java.util.concurrent.ExecutionException; boolean excludeMediaItems, boolean excludeMediaItemsMetadata, boolean excludeCues, - boolean excludeTimeline) + boolean excludeTimeline, + boolean excludeTracks) throws RemoteException { iController.onPlayerInfoChanged( seq, playerInfo.toBundle( - excludeMediaItems, excludeMediaItemsMetadata, excludeCues, excludeTimeline), + excludeMediaItems, + excludeMediaItemsMetadata, + excludeCues, + excludeTimeline, + excludeTracks), /* isTimelineExcluded= */ excludeTimeline); } diff --git a/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java b/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java index 6be5b3e5b7..f128a1ff0d 100644 --- a/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java +++ b/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java @@ -754,7 +754,8 @@ import java.lang.annotation.Target; boolean excludeMediaItems, boolean excludeMediaItemsMetadata, boolean excludeCues, - boolean excludeTimeline) { + boolean excludeTimeline, + boolean excludeTracks) { Bundle bundle = new Bundle(); if (playerError != null) { bundle.putBundle(keyForField(FIELD_PLAYBACK_ERROR), playerError.toBundle()); @@ -794,7 +795,9 @@ import java.lang.annotation.Target; bundle.putLong(keyForField(FIELD_SEEK_FORWARD_INCREMENT_MS), seekForwardIncrementMs); bundle.putLong( keyForField(FIELD_MAX_SEEK_TO_PREVIOUS_POSITION_MS), maxSeekToPreviousPositionMs); - bundle.putBundle(keyForField(FIELD_CURRENT_TRACKS), currentTracks.toBundle()); + if (!excludeTracks) { + bundle.putBundle(keyForField(FIELD_CURRENT_TRACKS), currentTracks.toBundle()); + } bundle.putBundle( keyForField(FIELD_TRACK_SELECTION_PARAMETERS), trackSelectionParameters.toBundle()); @@ -807,7 +810,8 @@ import java.lang.annotation.Target; /* excludeMediaItems= */ false, /* excludeMediaItemsMetadata= */ false, /* excludeCues= */ false, - /* excludeTimeline= */ false); + /* excludeTimeline= */ false, + /* excludeTracks= */ false); } /** Object that can restore {@link PlayerInfo} from a {@link Bundle}. */ 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 bd08f997ca..8b15263fea 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 @@ -26,10 +26,12 @@ public class MediaSessionConstants { 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"; + public static final String TEST_COMMAND_GET_TRACKS = "testCommandGetTracksUnavailable"; // Bundle keys public static final String KEY_AVAILABLE_SESSION_COMMANDS = "availableSessionCommands"; public static final String KEY_CONTROLLER = "controllerKey"; + public static final String KEY_COMMAND_GET_TASKS_UNAVAILABLE = "commandGetTasksUnavailable"; private MediaSessionConstants() {} } 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 904f4a8bc4..c3e9f6c058 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 @@ -29,7 +29,9 @@ 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.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.KEY_COMMAND_GET_TASKS_UNAVAILABLE; import static androidx.media3.test.session.common.MediaSessionConstants.KEY_CONTROLLER; +import static androidx.media3.test.session.common.MediaSessionConstants.TEST_COMMAND_GET_TRACKS; 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; @@ -878,6 +880,101 @@ public class MediaControllerListenerTest { assertThat(changedCurrentTracksFromGetterRef.get()).isEqualTo(currentTracks); } + @Test + public void getCurrentTracks_commandGetTracksUnavailable_currentTracksEmpty() throws Exception { + RemoteMediaSession remoteSession = createRemoteMediaSession(TEST_COMMAND_GET_TRACKS); + RemoteMediaSession.RemoteMockPlayer player = remoteSession.getMockPlayer(); + CountDownLatch latch = new CountDownLatch(2); + // A controller with the COMMAND_GET_TRACKS unavailable. + Bundle connectionHints = new Bundle(); + connectionHints.putBoolean(KEY_COMMAND_GET_TASKS_UNAVAILABLE, true); + MediaController controller = + controllerTestRule.createController( + remoteSession.getToken(), connectionHints, /* listener= */ null); + List capturedCurrentTracks = new ArrayList<>(); + Player.Listener listener = + new Player.Listener() { + @Override + public void onEvents(Player player, Player.Events events) { + capturedCurrentTracks.add(controller.getCurrentTracks()); + latch.countDown(); + } + }; + // A controller with the COMMAND_GET_TRACKS available. + MediaController controllerWithCommandAvailable = + controllerTestRule.createController(remoteSession.getToken()); + AtomicReference capturedCurrentTracksWithCommandAvailable = new AtomicReference<>(); + Player.Listener listenerWithCommandAvailable = + new Player.Listener() { + @Override + public void onEvents(Player player, Player.Events events) { + capturedCurrentTracksWithCommandAvailable.set(player.getCurrentTracks()); + latch.countDown(); + } + }; + AtomicReference initialCurrentTracks = new AtomicReference<>(); + AtomicReference initialCurrentTracksWithCommandAvailable = new AtomicReference<>(); + threadTestRule + .getHandler() + .postAndSync( + () -> { + initialCurrentTracks.set(controller.getCurrentTracks()); + initialCurrentTracksWithCommandAvailable.set( + controllerWithCommandAvailable.getCurrentTracks()); + controller.addListener(listener); + controllerWithCommandAvailable.addListener(listenerWithCommandAvailable); + }); + + player.notifyIsLoadingChanged(true); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(initialCurrentTracks.get()).isEqualTo(Tracks.EMPTY); + assertThat(capturedCurrentTracks).containsExactly(Tracks.EMPTY); + assertThat(initialCurrentTracksWithCommandAvailable.get().getGroups()).hasSize(1); + assertThat(capturedCurrentTracksWithCommandAvailable.get().getGroups()).hasSize(1); + } + + @Test + public void getCurrentTracks_commandGetTracksBecomesUnavailable_tracksResetToEmpty() + throws Exception { + RemoteMediaSession remoteSession = createRemoteMediaSession(TEST_COMMAND_GET_TRACKS); + RemoteMediaSession.RemoteMockPlayer player = remoteSession.getMockPlayer(); + CountDownLatch latch = new CountDownLatch(2); + // A controller with the COMMAND_GET_TRACKS available. + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + List capturedCurrentTracks = new ArrayList<>(); + Player.Listener listener = + new Player.Listener() { + @Override + public void onAvailableCommandsChanged(Commands availableCommands) { + capturedCurrentTracks.add(controller.getCurrentTracks()); + latch.countDown(); + } + + @Override + public void onTracksChanged(Tracks tracks) { + // The track change as a result of the available command change is notified second. + capturedCurrentTracks.add(controller.getCurrentTracks()); + latch.countDown(); + } + }; + AtomicReference availableCommands = new AtomicReference<>(); + threadTestRule + .getHandler() + .postAndSync( + () -> { + availableCommands.set(controller.getAvailableCommands()); + controller.addListener(listener); + }); + + player.notifyAvailableCommandsChanged( + availableCommands.get().buildUpon().remove(Player.COMMAND_GET_TRACKS).build()); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(capturedCurrentTracks.get(0).getGroups()).hasSize(1); + assertThat(capturedCurrentTracks.get(1)).isEqualTo(Tracks.EMPTY); + } + /** This also tests {@link MediaController#getShuffleModeEnabled()}. */ @Test public void onShuffleModeEnabledChanged() throws Exception { 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 9336008501..ee5be83e47 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 @@ -15,6 +15,7 @@ */ package androidx.media3.session; +import static androidx.media3.common.Player.COMMAND_GET_TRACKS; import static androidx.media3.test.session.common.CommonConstants.ACTION_MEDIA3_SESSION; import static androidx.media3.test.session.common.CommonConstants.KEY_AUDIO_ATTRIBUTES; import static androidx.media3.test.session.common.CommonConstants.KEY_BUFFERED_PERCENTAGE; @@ -55,7 +56,9 @@ import static androidx.media3.test.session.common.CommonConstants.KEY_TRACK_SELE import static androidx.media3.test.session.common.CommonConstants.KEY_VIDEO_SIZE; import static androidx.media3.test.session.common.CommonConstants.KEY_VOLUME; import static androidx.media3.test.session.common.MediaSessionConstants.KEY_AVAILABLE_SESSION_COMMANDS; +import static androidx.media3.test.session.common.MediaSessionConstants.KEY_COMMAND_GET_TASKS_UNAVAILABLE; import static androidx.media3.test.session.common.MediaSessionConstants.KEY_CONTROLLER; +import static androidx.media3.test.session.common.MediaSessionConstants.TEST_COMMAND_GET_TRACKS; 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; @@ -71,6 +74,7 @@ import androidx.annotation.Nullable; import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; import androidx.media3.common.DeviceInfo; +import androidx.media3.common.Format; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaMetadata; import androidx.media3.common.PlaybackException; @@ -79,6 +83,7 @@ import androidx.media3.common.Player; import androidx.media3.common.Player.DiscontinuityReason; import androidx.media3.common.Player.PositionInfo; import androidx.media3.common.Timeline; +import androidx.media3.common.TrackGroup; import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.Tracks; import androidx.media3.common.VideoSize; @@ -159,11 +164,10 @@ public class MediaSessionProviderService extends Service { @Override public void create(String sessionId, Bundle tokenExtras) throws RemoteException { + MockPlayer mockPlayer = + new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build(); MediaSession.Builder builder = - new MediaSession.Builder( - MediaSessionProviderService.this, - new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build()) - .setId(sessionId); + new MediaSession.Builder(MediaSessionProviderService.this, mockPlayer).setId(sessionId); if (tokenExtras != null) { builder.setExtras(tokenExtras); @@ -229,6 +233,34 @@ public class MediaSessionProviderService extends Service { }); break; } + case TEST_COMMAND_GET_TRACKS: + { + ImmutableList trackGroups = + ImmutableList.of( + new Tracks.Group( + new TrackGroup(new Format.Builder().setChannelCount(2).build()), + /* adaptiveSupported= */ false, + /* trackSupport= */ new int[1], + /* trackSelected= */ new boolean[1])); + mockPlayer.currentTracks = new Tracks(trackGroups); + builder.setCallback( + new MediaSession.Callback() { + @Override + public MediaSession.ConnectionResult onConnect( + MediaSession session, ControllerInfo controller) { + Player.Commands.Builder commandBuilder = + new Player.Commands.Builder().addAllCommands(); + if (controller + .getConnectionHints() + .getBoolean(KEY_COMMAND_GET_TASKS_UNAVAILABLE, /* defaultValue= */ false)) { + commandBuilder.remove(COMMAND_GET_TRACKS); + } + return MediaSession.ConnectionResult.accept( + SessionCommands.EMPTY, commandBuilder.build()); + } + }); + break; + } default: // fall out }