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 921d5acf36..9803450241 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -1290,7 +1290,7 @@ import org.checkerframework.checker.initialization.qual.Initialized; if (msg.what == MSG_PLAYER_INFO_CHANGED) { playerInfo = playerInfo.copyWithTimelineAndSessionPositionInfo( - getPlayerWrapper().getCurrentTimeline(), + getPlayerWrapper().getCurrentTimelineWithCommandCheck(), getPlayerWrapper().createSessionPositionInfoForBundling()); dispatchOnPlayerInfoChanged(playerInfo, excludeTimeline, excludeTracks); excludeTimeline = true; diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index 3c25022e9d..e130763ea2 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -23,7 +23,9 @@ import static androidx.media3.common.Player.COMMAND_SEEK_FORWARD; import static androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SEEK_TO_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT; +import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS; +import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE; import static androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE; @@ -256,10 +258,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; || playbackState == STATE_ENDED || playbackState == STATE_IDLE) { if (playbackState == STATE_IDLE) { - playerWrapper.prepare(); + playerWrapper.prepareIfCommandAvailable(); } else if (playbackState == STATE_ENDED) { - playerWrapper.seekTo( - playerWrapper.getCurrentMediaItemIndex(), /* positionMs= */ C.TIME_UNSET); + playerWrapper.seekToDefaultPositionIfCommandAvailable(); } playerWrapper.play(); } else { @@ -308,10 +309,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; PlayerWrapper playerWrapper = sessionImpl.getPlayerWrapper(); @Player.State int playbackState = playerWrapper.getPlaybackState(); if (playbackState == Player.STATE_IDLE) { - playerWrapper.prepare(); + playerWrapper.prepareIfCommandAvailable(); } else if (playbackState == Player.STATE_ENDED) { - playerWrapper.seekTo( - playerWrapper.getCurrentMediaItemIndex(), /* positionMs= */ C.TIME_UNSET); + playerWrapper.seekToDefaultPositionIfCommandAvailable(); } if (sessionImpl.onPlayRequested()) { playerWrapper.play(); @@ -369,18 +369,32 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Override public void onSkipToNext() { - dispatchSessionTaskWithPlayerCommand( - COMMAND_SEEK_TO_NEXT, - controller -> sessionImpl.getPlayerWrapper().seekToNext(), - sessionCompat.getCurrentControllerInfo()); + if (sessionImpl.getPlayerWrapper().isCommandAvailable(COMMAND_SEEK_TO_NEXT)) { + dispatchSessionTaskWithPlayerCommand( + COMMAND_SEEK_TO_NEXT, + controller -> sessionImpl.getPlayerWrapper().seekToNext(), + sessionCompat.getCurrentControllerInfo()); + } else { + dispatchSessionTaskWithPlayerCommand( + COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, + controller -> sessionImpl.getPlayerWrapper().seekToNextMediaItem(), + sessionCompat.getCurrentControllerInfo()); + } } @Override public void onSkipToPrevious() { - dispatchSessionTaskWithPlayerCommand( - COMMAND_SEEK_TO_PREVIOUS, - controller -> sessionImpl.getPlayerWrapper().seekToPrevious(), - sessionCompat.getCurrentControllerInfo()); + if (sessionImpl.getPlayerWrapper().isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS)) { + dispatchSessionTaskWithPlayerCommand( + COMMAND_SEEK_TO_PREVIOUS, + controller -> sessionImpl.getPlayerWrapper().seekToPrevious(), + sessionCompat.getCurrentControllerInfo()); + } else { + dispatchSessionTaskWithPlayerCommand( + COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, + controller -> sessionImpl.getPlayerWrapper().seekToPreviousMediaItem(), + sessionCompat.getCurrentControllerInfo()); + } } @Override @@ -435,7 +449,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; dispatchSessionTaskWithSessionCommand( SessionCommand.COMMAND_CODE_SESSION_SET_RATING, controller -> { - @Nullable MediaItem currentItem = sessionImpl.getPlayerWrapper().getCurrentMediaItem(); + @Nullable + MediaItem currentItem = + sessionImpl.getPlayerWrapper().getCurrentMediaItemWithCommandCheck(); if (currentItem == null) { return; } @@ -494,12 +510,17 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; Log.w(TAG, "onRemoveQueueItem(): Media ID shouldn't be null"); return; } - Timeline timeline = sessionImpl.getPlayerWrapper().getCurrentTimeline(); + PlayerWrapper player = sessionImpl.getPlayerWrapper(); + if (!player.isCommandAvailable(Player.COMMAND_GET_TIMELINE)) { + Log.w(TAG, "Can't remove item by id without availabe COMMAND_GET_TIMELINE"); + return; + } + Timeline timeline = player.getCurrentTimeline(); Timeline.Window window = new Timeline.Window(); for (int i = 0; i < timeline.getWindowCount(); i++) { MediaItem mediaItem = timeline.getWindow(i, window).mediaItem; if (TextUtils.equals(mediaItem.mediaId, mediaId)) { - sessionImpl.getPlayerWrapper().removeMediaItem(i); + player.removeMediaItem(i); return; } } @@ -700,16 +721,16 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; postOrRun( sessionImpl.getApplicationHandler(), () -> { - Player player = sessionImpl.getPlayerWrapper(); + PlayerWrapper player = sessionImpl.getPlayerWrapper(); player.setMediaItems(mediaItems); @Player.State int playbackState = player.getPlaybackState(); if (playbackState == Player.STATE_IDLE) { - player.prepare(); + player.prepareIfCommandAvailable(); } else if (playbackState == Player.STATE_ENDED) { - player.seekTo(/* positionMs= */ C.TIME_UNSET); + player.seekToDefaultPositionIfCommandAvailable(); } if (play) { - player.play(); + player.playIfCommandAvailable(); } }); } @@ -875,19 +896,21 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; throws RemoteException { // Tells the playlist change first, so current media item index change notification // can point to the valid current media item in the playlist. - Timeline newTimeline = newPlayerWrapper.getCurrentTimeline(); + Timeline newTimeline = newPlayerWrapper.getCurrentTimelineWithCommandCheck(); if (oldPlayerWrapper == null - || !Util.areEqual(oldPlayerWrapper.getCurrentTimeline(), newTimeline)) { + || !Util.areEqual(oldPlayerWrapper.getCurrentTimelineWithCommandCheck(), newTimeline)) { onTimelineChanged(seq, newTimeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); } - MediaMetadata newPlaylistMetadata = newPlayerWrapper.getPlaylistMetadata(); + MediaMetadata newPlaylistMetadata = newPlayerWrapper.getPlaylistMetadataWithCommandCheck(); if (oldPlayerWrapper == null - || !Util.areEqual(oldPlayerWrapper.getPlaylistMetadata(), newPlaylistMetadata)) { + || !Util.areEqual( + oldPlayerWrapper.getPlaylistMetadataWithCommandCheck(), newPlaylistMetadata)) { onPlaylistMetadataChanged(seq, newPlaylistMetadata); } - MediaMetadata newMediaMetadata = newPlayerWrapper.getMediaMetadata(); + MediaMetadata newMediaMetadata = newPlayerWrapper.getMediaMetadataWithCommandCheck(); if (oldPlayerWrapper == null - || !Util.areEqual(oldPlayerWrapper.getMediaMetadata(), newMediaMetadata)) { + || !Util.areEqual( + oldPlayerWrapper.getMediaMetadataWithCommandCheck(), newMediaMetadata)) { onMediaMetadataChanged(seq, newMediaMetadata); } if (oldPlayerWrapper == null @@ -904,9 +927,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; onDeviceInfoChanged(seq, newPlayerWrapper.getDeviceInfo()); // Rest of changes are all notified via PlaybackStateCompat. - @Nullable MediaItem newMediaItem = newPlayerWrapper.getCurrentMediaItem(); + @Nullable MediaItem newMediaItem = newPlayerWrapper.getCurrentMediaItemWithCommandCheck(); if (oldPlayerWrapper == null - || !Util.areEqual(oldPlayerWrapper.getCurrentMediaItem(), newMediaItem)) { + || !Util.areEqual(oldPlayerWrapper.getCurrentMediaItemWithCommandCheck(), newMediaItem)) { // Note: This will update both PlaybackStateCompat and metadata. onMediaItemTransition( seq, newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); @@ -1135,7 +1158,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; PlayerWrapper player = sessionImpl.getPlayerWrapper(); volumeProviderCompat = player.createVolumeProviderCompat(); if (volumeProviderCompat == null) { - int streamType = MediaUtils.getLegacyStreamType(player.getAudioAttributes()); + int streamType = + MediaUtils.getLegacyStreamType(player.getAudioAttributesWithCommandCheck()); sessionCompat.setPlaybackToLocal(streamType); } else { sessionCompat.setPlaybackToRemote(volumeProviderCompat); @@ -1158,10 +1182,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } private void updateMetadataIfChanged() { - Player player = sessionImpl.getPlayerWrapper(); - @Nullable MediaItem currentMediaItem = player.getCurrentMediaItem(); - MediaMetadata newMediaMetadata = player.getMediaMetadata(); - long newDurationMs = player.getDuration(); + PlayerWrapper player = sessionImpl.getPlayerWrapper(); + @Nullable MediaItem currentMediaItem = player.getCurrentMediaItemWithCommandCheck(); + MediaMetadata newMediaMetadata = player.getMediaMetadataWithCommandCheck(); + long newDurationMs = player.getDurationWithCommandCheck(); String newMediaId = currentMediaItem != null ? currentMediaItem.mediaId : MediaItem.DEFAULT_MEDIA_ID; @Nullable diff --git a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java index a9eab3f1a3..97a85e3ffb 100644 --- a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java +++ b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java @@ -26,6 +26,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; +import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import android.view.Surface; import android.view.SurfaceHolder; @@ -34,6 +35,7 @@ import android.view.TextureView; import androidx.annotation.Nullable; import androidx.media.VolumeProviderCompat; import androidx.media3.common.AudioAttributes; +import androidx.media3.common.C; import androidx.media3.common.DeviceInfo; import androidx.media3.common.ForwardingPlayer; import androidx.media3.common.MediaItem; @@ -43,9 +45,11 @@ import androidx.media3.common.PlaybackParameters; import androidx.media3.common.Player; import androidx.media3.common.Timeline; import androidx.media3.common.TrackSelectionParameters; +import androidx.media3.common.Tracks; import androidx.media3.common.VideoSize; import androidx.media3.common.text.CueGroup; import androidx.media3.common.util.Log; +import androidx.media3.common.util.Size; import androidx.media3.common.util.Util; import com.google.common.collect.ImmutableList; import java.util.List; @@ -133,6 +137,12 @@ import java.util.List; super.play(); } + public void playIfCommandAvailable() { + if (isCommandAvailable(COMMAND_PLAY_PAUSE)) { + play(); + } + } + @Override public void pause() { verifyApplicationThread(); @@ -145,6 +155,12 @@ import java.util.List; super.prepare(); } + public void prepareIfCommandAvailable() { + if (isCommandAvailable(COMMAND_PREPARE)) { + prepare(); + } + } + @Override public void stop() { verifyApplicationThread(); @@ -163,6 +179,18 @@ import java.util.List; super.seekToDefaultPosition(mediaItemIndex); } + @Override + public void seekToDefaultPosition() { + verifyApplicationThread(); + super.seekToDefaultPosition(); + } + + public void seekToDefaultPositionIfCommandAvailable() { + if (isCommandAvailable(Player.COMMAND_SEEK_TO_DEFAULT_POSITION)) { + seekToDefaultPosition(); + } + } + @Override public void seekTo(long positionMs) { verifyApplicationThread(); @@ -223,6 +251,10 @@ import java.util.List; return super.getDuration(); } + public long getDurationWithCommandCheck() { + return isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM) ? getDuration() : C.TIME_UNSET; + } + @Override public long getBufferedPosition() { verifyApplicationThread(); @@ -355,6 +387,12 @@ import java.util.List; return super.getAudioAttributes(); } + public AudioAttributes getAudioAttributesWithCommandCheck() { + return isCommandAvailable(COMMAND_GET_AUDIO_ATTRIBUTES) + ? getAudioAttributes() + : AudioAttributes.DEFAULT; + } + @Override public void setMediaItem(MediaItem mediaItem) { verifyApplicationThread(); @@ -549,12 +587,22 @@ import java.util.List; return super.getCurrentTimeline(); } + public Timeline getCurrentTimelineWithCommandCheck() { + return isCommandAvailable(COMMAND_GET_TIMELINE) ? getCurrentTimeline() : Timeline.EMPTY; + } + @Override public MediaMetadata getPlaylistMetadata() { verifyApplicationThread(); return super.getPlaylistMetadata(); } + public MediaMetadata getPlaylistMetadataWithCommandCheck() { + return isCommandAvailable(Player.COMMAND_GET_MEDIA_ITEMS_METADATA) + ? getPlaylistMetadata() + : MediaMetadata.EMPTY; + } + @Override public int getRepeatMode() { verifyApplicationThread(); @@ -574,6 +622,11 @@ import java.util.List; return super.getCurrentMediaItem(); } + @Nullable + public MediaItem getCurrentMediaItemWithCommandCheck() { + return isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM) ? getCurrentMediaItem() : null; + } + @Override public int getMediaItemCount() { verifyApplicationThread(); @@ -631,6 +684,10 @@ import java.util.List; return super.getVolume(); } + public float getVolumeWithCommandCheck() { + return isCommandAvailable(COMMAND_GET_VOLUME) ? getVolume() : 0; + } + @Override public void setVolume(float volume) { verifyApplicationThread(); @@ -643,6 +700,10 @@ import java.util.List; return super.getCurrentCues(); } + public CueGroup getCurrentCuesWithCommandCheck() { + return isCommandAvailable(COMMAND_GET_TEXT) ? getCurrentCues() : CueGroup.EMPTY_TIME_ZERO; + } + @Override public DeviceInfo getDeviceInfo() { verifyApplicationThread(); @@ -655,18 +716,32 @@ import java.util.List; return super.getDeviceVolume(); } + public int getDeviceVolumeWithCommandCheck() { + return isCommandAvailable(COMMAND_GET_DEVICE_VOLUME) ? getDeviceVolume() : 0; + } + @Override public boolean isDeviceMuted() { verifyApplicationThread(); return super.isDeviceMuted(); } + public boolean isDeviceMutedWithCommandCheck() { + return isCommandAvailable(Player.COMMAND_GET_DEVICE_VOLUME) && isDeviceMuted(); + } + @Override public void setDeviceVolume(int volume) { verifyApplicationThread(); super.setDeviceVolume(volume); } + public void setDeviceVolumeIfCommandAvailable(int volume) { + if (isCommandAvailable(COMMAND_SET_DEVICE_VOLUME)) { + setDeviceVolume(volume); + } + } + @Override public void increaseDeviceVolume() { verifyApplicationThread(); @@ -729,6 +804,12 @@ import java.util.List; return super.getMediaMetadata(); } + public MediaMetadata getMediaMetadataWithCommandCheck() { + return isCommandAvailable(COMMAND_GET_MEDIA_ITEMS_METADATA) + ? getMediaMetadata() + : MediaMetadata.EMPTY; + } + @Override public boolean isCommandAvailable(@Command int command) { verifyApplicationThread(); @@ -753,6 +834,71 @@ import java.util.List; super.setTrackSelectionParameters(parameters); } + @Override + public void seekToPrevious() { + verifyApplicationThread(); + super.seekToPrevious(); + } + + @Override + public long getMaxSeekToPreviousPosition() { + verifyApplicationThread(); + return super.getMaxSeekToPreviousPosition(); + } + + @Override + public void seekToNext() { + verifyApplicationThread(); + super.seekToNext(); + } + + @Override + public Tracks getCurrentTracks() { + verifyApplicationThread(); + return super.getCurrentTracks(); + } + + public Tracks getCurrentTracksWithCommandCheck() { + return isCommandAvailable(COMMAND_GET_TRACKS) ? getCurrentTracks() : Tracks.EMPTY; + } + + @Nullable + @Override + public Object getCurrentManifest() { + verifyApplicationThread(); + return super.getCurrentManifest(); + } + + @Override + public int getCurrentPeriodIndex() { + verifyApplicationThread(); + return super.getCurrentPeriodIndex(); + } + + @Override + public boolean isCurrentMediaItemDynamic() { + verifyApplicationThread(); + return super.isCurrentMediaItemDynamic(); + } + + @Override + public boolean isCurrentMediaItemLive() { + verifyApplicationThread(); + return super.isCurrentMediaItemLive(); + } + + @Override + public boolean isCurrentMediaItemSeekable() { + verifyApplicationThread(); + return super.isCurrentMediaItemSeekable(); + } + + @Override + public Size getSurfaceSize() { + verifyApplicationThread(); + return super.getSurfaceSize(); + } + public PlaybackStateCompat createPlaybackStateCompat() { if (legacyStatusCode != STATUS_CODE_SUCCESS_COMPAT) { return new PlaybackStateCompat.Builder() @@ -799,22 +945,28 @@ import java.util.List; || getAvailableCommands().contains(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)) { allActions |= PlaybackStateCompat.ACTION_SKIP_TO_NEXT; } - long queueItemId = MediaUtils.convertToQueueItemId(getCurrentMediaItemIndex()); + long queueItemId = + isCommandAvailable(COMMAND_GET_TIMELINE) + ? MediaUtils.convertToQueueItemId(getCurrentMediaItemIndex()) + : MediaSessionCompat.QueueItem.UNKNOWN_ID; float playbackSpeed = getPlaybackParameters().speed; float sessionPlaybackSpeed = isPlaying() ? playbackSpeed : 0f; Bundle extras = new Bundle(); extras.putFloat(EXTRAS_KEY_PLAYBACK_SPEED_COMPAT, playbackSpeed); - @Nullable MediaItem currentMediaItem = getCurrentMediaItem(); + @Nullable MediaItem currentMediaItem = getCurrentMediaItemWithCommandCheck(); if (currentMediaItem != null && !MediaItem.DEFAULT_MEDIA_ID.equals(currentMediaItem.mediaId)) { extras.putString(EXTRAS_KEY_MEDIA_ID_COMPAT, currentMediaItem.mediaId); } + boolean canReadPositions = isCommandAvailable(Player.COMMAND_GET_CURRENT_MEDIA_ITEM); + long compatPosition = + canReadPositions ? getCurrentPosition() : PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN; + long compatBufferedPosition = canReadPositions ? getBufferedPosition() : 0; PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder() - .setState( - state, getCurrentPosition(), sessionPlaybackSpeed, SystemClock.elapsedRealtime()) + .setState(state, compatPosition, sessionPlaybackSpeed, SystemClock.elapsedRealtime()) .setActions(allActions) .setActiveQueueItemId(queueItemId) - .setBufferedPosition(getBufferedPosition()) + .setBufferedPosition(compatBufferedPosition) .setExtras(extras); for (int i = 0; i < customLayout.size(); i++) { @@ -853,11 +1005,11 @@ import java.util.List; } } Handler handler = new Handler(getApplicationLooper()); - return new VolumeProviderCompat( - volumeControlType, getDeviceInfo().maxVolume, getDeviceVolume()) { + int currentVolume = getDeviceVolumeWithCommandCheck(); + return new VolumeProviderCompat(volumeControlType, getDeviceInfo().maxVolume, currentVolume) { @Override public void onSetVolumeTo(int volume) { - postOrRun(handler, () -> setDeviceVolume(volume)); + postOrRun(handler, () -> setDeviceVolumeIfCommandAvailable(volume)); } @Override @@ -865,6 +1017,9 @@ import java.util.List; postOrRun( handler, () -> { + if (!isCommandAvailable(COMMAND_ADJUST_DEVICE_VOLUME)) { + return; + } switch (direction) { case AudioManager.ADJUST_RAISE: increaseDeviceVolume(); @@ -879,7 +1034,7 @@ import java.util.List; setDeviceMuted(false); break; case AudioManager.ADJUST_TOGGLE_MUTE: - setDeviceMuted(!isDeviceMuted()); + setDeviceMuted(!isDeviceMutedWithCommandCheck()); break; default: Log.w( @@ -898,6 +1053,9 @@ import java.util.List; *

This excludes window uid and period uid that wouldn't be preserved when bundling. */ public PositionInfo createPositionInfoForBundling() { + if (!isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM)) { + return SessionPositionInfo.DEFAULT_POSITION_INFO; + } return new PositionInfo( /* windowUid= */ null, getCurrentMediaItemIndex(), @@ -916,6 +1074,9 @@ import java.util.List; *

This excludes window uid and period uid that wouldn't be preserved when bundling. */ public SessionPositionInfo createSessionPositionInfoForBundling() { + if (!isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM)) { + return SessionPositionInfo.DEFAULT; + } return new SessionPositionInfo( createPositionInfoForBundling(), isPlayingAd(), @@ -941,25 +1102,25 @@ import java.util.List; getRepeatMode(), getShuffleModeEnabled(), getVideoSize(), - getCurrentTimeline(), - getPlaylistMetadata(), - getVolume(), - getAudioAttributes(), - getCurrentCues(), + getCurrentTimelineWithCommandCheck(), + getPlaylistMetadataWithCommandCheck(), + getVolumeWithCommandCheck(), + getAudioAttributesWithCommandCheck(), + getCurrentCuesWithCommandCheck(), getDeviceInfo(), - getDeviceVolume(), - isDeviceMuted(), + getDeviceVolumeWithCommandCheck(), + isDeviceMutedWithCommandCheck(), getPlayWhenReady(), PlayerInfo.PLAY_WHEN_READY_CHANGE_REASON_DEFAULT, getPlaybackSuppressionReason(), getPlaybackState(), isPlaying(), isLoading(), - getMediaMetadata(), + getMediaMetadataWithCommandCheck(), getSeekBackIncrement(), getSeekForwardIncrement(), getMaxSeekToPreviousPosition(), - getCurrentTracks(), + getCurrentTracksWithCommandCheck(), getTrackSelectionParameters()); } diff --git a/libraries/session/src/test/java/androidx/media3/session/PlayerWrapperTest.java b/libraries/session/src/test/java/androidx/media3/session/PlayerWrapperTest.java index 85e26f4cc3..430e14c61c 100644 --- a/libraries/session/src/test/java/androidx/media3/session/PlayerWrapperTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/PlayerWrapperTest.java @@ -16,6 +16,7 @@ package androidx.media3.session; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.when; import android.os.Looper; @@ -42,6 +43,7 @@ public class PlayerWrapperTest { @Before public void setUp() { playerWrapper = new PlayerWrapper(player); + when(player.isCommandAvailable(anyInt())).thenReturn(true); when(player.getApplicationLooper()).thenReturn(Looper.myLooper()); } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java index 6fd12bc37e..504c48ed63 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java @@ -18,7 +18,9 @@ package androidx.media3.session; import static androidx.media.MediaSessionManager.RemoteUserInfo.LEGACY_CONTROLLER; import static androidx.media3.common.Player.COMMAND_PLAY_PAUSE; import static androidx.media3.common.Player.COMMAND_PREPARE; +import static androidx.media3.common.Player.STATE_ENDED; import static androidx.media3.common.Player.STATE_IDLE; +import static androidx.media3.common.Player.STATE_READY; import static androidx.media3.session.SessionResult.RESULT_ERROR_INVALID_STATE; import static androidx.media3.session.SessionResult.RESULT_SUCCESS; import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; @@ -204,7 +206,8 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { } @Test - public void play() throws Exception { + public void play_whileReady_callsPlay() throws Exception { + player.playbackState = STATE_READY; session = new MediaSession.Builder(context, player) .setId("play") @@ -217,6 +220,92 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller.getTransportControls().play(); player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isFalse(); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SEEK_TO_DEFAULT_POSITION)).isFalse(); + } + + @Test + public void play_whileIdle_callsPrepareAndPlay() throws Exception { + player.playbackState = STATE_IDLE; + session = + new MediaSession.Builder(context, player) + .setId("play") + .setCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + + controller.getTransportControls().play(); + + player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SEEK_TO_DEFAULT_POSITION)).isFalse(); + } + + @Test + public void play_whileIdleWithoutPrepareCommandAvailable_callsJustPlay() throws Exception { + player.playbackState = STATE_IDLE; + player.commands = + new Player.Commands.Builder().addAllCommands().remove(Player.COMMAND_PREPARE).build(); + session = + new MediaSession.Builder(context, player) + .setId("play") + .setCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + + controller.getTransportControls().play(); + + player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isFalse(); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SEEK_TO_DEFAULT_POSITION)).isFalse(); + } + + @Test + public void play_whileEnded_callsSeekToDefaultPositionAndPlay() throws Exception { + player.playbackState = STATE_ENDED; + session = + new MediaSession.Builder(context, player) + .setId("play") + .setCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + + controller.getTransportControls().play(); + + player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_DEFAULT_POSITION, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isFalse(); + } + + @Test + public void play_whileEndedWithoutSeekToDefaultPositionCommandAvailable_callsJustPlay() + throws Exception { + player.playbackState = STATE_ENDED; + player.commands = + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SEEK_TO_DEFAULT_POSITION) + .build(); + session = + new MediaSession.Builder(context, player) + .setId("play") + .setCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + + controller.getTransportControls().play(); + + player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SEEK_TO_DEFAULT_POSITION)).isFalse(); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isFalse(); } @Test @@ -428,7 +517,7 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { } @Test - public void skipToPrevious() throws Exception { + public void skipToPrevious_withAllCommandsAvailable_callsSeekToPrevious() throws Exception { session = new MediaSession.Builder(context, player) .setId("skipToPrevious") @@ -444,7 +533,29 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { } @Test - public void skipToNext() throws Exception { + public void skipToPrevious_withoutSeekToPreviousCommandAvailable_callsSeekToPreviousMediaItem() + throws Exception { + player.commands = + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SEEK_TO_PREVIOUS) + .build(); + session = + new MediaSession.Builder(context, player) + .setId("skipToPrevious") + .setCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + + controller.getTransportControls().skipToPrevious(); + + player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_PREVIOUS_MEDIA_ITEM, TIMEOUT_MS); + } + + @Test + public void skipToNext_withAllCommandsAvailable_callsSeekToNext() throws Exception { session = new MediaSession.Builder(context, player) .setId("skipToNext") @@ -459,6 +570,25 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_NEXT, TIMEOUT_MS); } + @Test + public void skipToNext_withoutSeekToNextCommandAvailable_callsSeekToNextMediaItem() + throws Exception { + player.commands = + new Player.Commands.Builder().addAllCommands().remove(Player.COMMAND_SEEK_TO_NEXT).build(); + session = + new MediaSession.Builder(context, player) + .setId("skipToNext") + .setCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + + controller.getTransportControls().skipToNext(); + + player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_NEXT_MEDIA_ITEM, TIMEOUT_MS); + } + @Test public void skipToQueueItem() throws Exception { session = @@ -1049,6 +1179,101 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { assertThat(player.mediaItems).containsExactly(resolvedMediaItem); } + @Test + public void prepareFromMediaUri_withoutAvailablePrepareCommand_justCallsSetMediaItems() + throws Exception { + MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI); + MediaSession.Callback callback = + new MediaSession.Callback() { + @Override + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, ControllerInfo controller, List mediaItems) { + return Futures.immediateFuture(ImmutableList.of(resolvedMediaItem)); + } + }; + player.commands = + new Player.Commands.Builder().addAllCommands().remove(COMMAND_PREPARE).build(); + session = + new MediaSession.Builder(context, player) + .setId("prepareFromMediaUri") + .setCallback(callback) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + + controller.getTransportControls().prepareFromUri(Uri.parse("foo://bar"), Bundle.EMPTY); + + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isFalse(); + assertThat(player.mediaItems).containsExactly(resolvedMediaItem); + } + + @Test + public void playFromMediaUri_withoutAvailablePrepareCommand_justCallsSetMediaItemsAndPlay() + throws Exception { + MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI); + MediaSession.Callback callback = + new MediaSession.Callback() { + @Override + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, ControllerInfo controller, List mediaItems) { + return Futures.immediateFuture(ImmutableList.of(resolvedMediaItem)); + } + }; + player.commands = + new Player.Commands.Builder().addAllCommands().remove(COMMAND_PREPARE).build(); + session = + new MediaSession.Builder(context, player) + .setId("prepareFromMediaUri") + .setCallback(callback) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + + controller.getTransportControls().playFromUri(Uri.parse("foo://bar"), Bundle.EMPTY); + + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isFalse(); + assertThat(player.mediaItems).containsExactly(resolvedMediaItem); + } + + @Test + public void playFromMediaUri_withoutAvailablePrepareAndPlayCommand_justCallsSetMediaItems() + throws Exception { + MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI); + MediaSession.Callback callback = + new MediaSession.Callback() { + @Override + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, ControllerInfo controller, List mediaItems) { + return Futures.immediateFuture(ImmutableList.of(resolvedMediaItem)); + } + }; + player.commands = + new Player.Commands.Builder() + .addAllCommands() + .removeAll(COMMAND_PREPARE, COMMAND_PLAY_PAUSE) + .build(); + session = + new MediaSession.Builder(context, player) + .setId("prepareFromMediaUri") + .setCallback(callback) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + + controller.getTransportControls().playFromUri(Uri.parse("foo://bar"), Bundle.EMPTY); + + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isFalse(); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PLAY)).isFalse(); + assertThat(player.mediaItems).containsExactly(resolvedMediaItem); + } + @Test public void setRating() throws Exception { int ratingType = RatingCompat.RATING_5_STARS; diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionKeyEventTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionKeyEventTest.java index a6a1f2327f..245f944972 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionKeyEventTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionKeyEventTest.java @@ -220,7 +220,7 @@ public class MediaSessionKeyEventTest { dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, false); - player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_WITH_MEDIA_ITEM_INDEX, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_DEFAULT_POSITION, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPermissionTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPermissionTest.java index 8d82012e32..d19743b6ee 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPermissionTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPermissionTest.java @@ -38,12 +38,19 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import android.content.Context; import android.os.Bundle; import android.text.TextUtils; +import androidx.annotation.Nullable; +import androidx.media3.common.AudioAttributes; +import androidx.media3.common.DeviceInfo; +import androidx.media3.common.ForwardingPlayer; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaMetadata; import androidx.media3.common.Player; import androidx.media3.common.Rating; import androidx.media3.common.StarRating; +import androidx.media3.common.Timeline; import androidx.media3.common.TrackSelectionParameters; +import androidx.media3.common.Tracks; +import androidx.media3.common.text.CueGroup; import androidx.media3.session.MediaSession.ControllerInfo; import androidx.media3.test.session.common.HandlerThreadTestRule; import androidx.media3.test.session.common.MainLooperTestRule; @@ -262,6 +269,168 @@ public class MediaSessionPermissionTest { TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT)); } + @Test + public void setPlayer_withoutAvailableCommands_doesNotCallProtectedPlayerGetters() + throws Exception { + MockPlayer mockPlayer = + new MockPlayer.Builder() + .setApplicationLooper(threadTestRule.getHandler().getLooper()) + .build(); + // Set remote device info to ensure we also cover the volume provider compat setup. + mockPlayer.deviceInfo = + new DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, /* minVolume= */ 0, /* maxVolume= */ 100); + Player player = + new ForwardingPlayer(mockPlayer) { + @Override + public boolean isCommandAvailable(int command) { + return false; + } + + @Override + public Tracks getCurrentTracks() { + throw new UnsupportedOperationException(); + } + + @Override + public MediaMetadata getMediaMetadata() { + throw new UnsupportedOperationException(); + } + + @Override + public MediaMetadata getPlaylistMetadata() { + throw new UnsupportedOperationException(); + } + + @Override + public Timeline getCurrentTimeline() { + throw new UnsupportedOperationException(); + } + + @Override + public int getCurrentPeriodIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public int getCurrentMediaItemIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public int getNextMediaItemIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public int getPreviousMediaItemIndex() { + throw new UnsupportedOperationException(); + } + + @Nullable + @Override + public MediaItem getCurrentMediaItem() { + throw new UnsupportedOperationException(); + } + + @Override + public long getDuration() { + throw new UnsupportedOperationException(); + } + + @Override + public long getCurrentPosition() { + throw new UnsupportedOperationException(); + } + + @Override + public long getBufferedPosition() { + throw new UnsupportedOperationException(); + } + + @Override + public long getTotalBufferedDuration() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isCurrentMediaItemDynamic() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isCurrentMediaItemLive() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isPlayingAd() { + throw new UnsupportedOperationException(); + } + + @Override + public int getCurrentAdGroupIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public int getCurrentAdIndexInAdGroup() { + throw new UnsupportedOperationException(); + } + + @Override + public long getContentDuration() { + throw new UnsupportedOperationException(); + } + + @Override + public long getContentPosition() { + throw new UnsupportedOperationException(); + } + + @Override + public long getContentBufferedPosition() { + throw new UnsupportedOperationException(); + } + + @Override + public AudioAttributes getAudioAttributes() { + throw new UnsupportedOperationException(); + } + + @Override + public CueGroup getCurrentCues() { + throw new UnsupportedOperationException(); + } + + @Override + public int getDeviceVolume() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isDeviceMuted() { + throw new UnsupportedOperationException(); + } + }; + MediaSession session = new MediaSession.Builder(context, player).setId(SESSION_ID).build(); + + MediaController controller = + new MediaController.Builder(context, session.getToken()) + .setApplicationLooper(threadTestRule.getHandler().getLooper()) + .buildAsync() + .get(); + + // Test passes if none of the protected player getters have been called. + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.release(); + session.release(); + player.release(); + }); + } + private ControllerInfo getTestControllerInfo() { List controllers = session.getConnectedControllers(); assertThat(controllers).isNotNull();