diff --git a/libraries/common/src/main/java/androidx/media3/common/Player.java b/libraries/common/src/main/java/androidx/media3/common/Player.java index 3a631280ca..9c444e5d97 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Player.java +++ b/libraries/common/src/main/java/androidx/media3/common/Player.java @@ -31,6 +31,7 @@ import androidx.annotation.FloatRange; import androidx.annotation.IntDef; import androidx.annotation.IntRange; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.media3.common.text.Cue; import androidx.media3.common.text.CueGroup; import androidx.media3.common.util.Size; @@ -350,15 +351,9 @@ public interface Player { return false; } PositionInfo that = (PositionInfo) o; - return mediaItemIndex == that.mediaItemIndex - && periodIndex == that.periodIndex - && positionMs == that.positionMs - && contentPositionMs == that.contentPositionMs - && adGroupIndex == that.adGroupIndex - && adIndexInAdGroup == that.adIndexInAdGroup + return equalsForBundling(that) && Objects.equal(windowUid, that.windowUid) - && Objects.equal(periodUid, that.periodUid) - && Objects.equal(mediaItem, that.mediaItem); + && Objects.equal(periodUid, that.periodUid); } @Override @@ -375,16 +370,97 @@ public interface Player { adIndexInAdGroup); } + /** + * Returns whether this position info and the other position info would result in the same + * {@link #toBundle() Bundle}. + */ + @UnstableApi + public boolean equalsForBundling(PositionInfo other) { + return mediaItemIndex == other.mediaItemIndex + && periodIndex == other.periodIndex + && positionMs == other.positionMs + && contentPositionMs == other.contentPositionMs + && adGroupIndex == other.adGroupIndex + && adIndexInAdGroup == other.adIndexInAdGroup + && Objects.equal(mediaItem, other.mediaItem); + } + // Bundleable implementation. - private static final String FIELD_MEDIA_ITEM_INDEX = Util.intToStringMaxRadix(0); + @VisibleForTesting static final String FIELD_MEDIA_ITEM_INDEX = Util.intToStringMaxRadix(0); private static final String FIELD_MEDIA_ITEM = Util.intToStringMaxRadix(1); - private static final String FIELD_PERIOD_INDEX = Util.intToStringMaxRadix(2); - private static final String FIELD_POSITION_MS = Util.intToStringMaxRadix(3); - private static final String FIELD_CONTENT_POSITION_MS = Util.intToStringMaxRadix(4); + @VisibleForTesting static final String FIELD_PERIOD_INDEX = Util.intToStringMaxRadix(2); + @VisibleForTesting static final String FIELD_POSITION_MS = Util.intToStringMaxRadix(3); + @VisibleForTesting static final String FIELD_CONTENT_POSITION_MS = Util.intToStringMaxRadix(4); private static final String FIELD_AD_GROUP_INDEX = Util.intToStringMaxRadix(5); private static final String FIELD_AD_INDEX_IN_AD_GROUP = Util.intToStringMaxRadix(6); + /** + * Returns a copy of this position info, filtered by the specified available commands. + * + *

The filtered fields are reset to their default values. + * + *

The return value may be the same object if nothing is filtered. + * + * @param canAccessCurrentMediaItem Whether {@link Player#COMMAND_GET_CURRENT_MEDIA_ITEM} is + * available. + * @param canAccessTimeline Whether {@link Player#COMMAND_GET_TIMELINE} is available. + * @return The filtered position info. + */ + @UnstableApi + public PositionInfo filterByAvailableCommands( + boolean canAccessCurrentMediaItem, boolean canAccessTimeline) { + if (canAccessCurrentMediaItem && canAccessTimeline) { + return this; + } + return new PositionInfo( + windowUid, + canAccessTimeline ? mediaItemIndex : 0, + canAccessCurrentMediaItem ? mediaItem : null, + periodUid, + canAccessTimeline ? periodIndex : 0, + canAccessCurrentMediaItem ? positionMs : 0, + canAccessCurrentMediaItem ? contentPositionMs : 0, + canAccessCurrentMediaItem ? adGroupIndex : C.INDEX_UNSET, + canAccessCurrentMediaItem ? adIndexInAdGroup : C.INDEX_UNSET); + } + + /** + * {@inheritDoc} + * + *

It omits the {@link #windowUid} and {@link #periodUid} fields. The {@link #windowUid} and + * {@link #periodUid} of an instance restored by {@link #CREATOR} will always be {@code null}. + * + * @param controllerInterfaceVersion The interface version of the media controller this Bundle + * will be sent to. + */ + @UnstableApi + public Bundle toBundle(int controllerInterfaceVersion) { + Bundle bundle = new Bundle(); + if (controllerInterfaceVersion < 3 || mediaItemIndex != 0) { + bundle.putInt(FIELD_MEDIA_ITEM_INDEX, mediaItemIndex); + } + if (mediaItem != null) { + bundle.putBundle(FIELD_MEDIA_ITEM, mediaItem.toBundle()); + } + if (controllerInterfaceVersion < 3 || periodIndex != 0) { + bundle.putInt(FIELD_PERIOD_INDEX, periodIndex); + } + if (controllerInterfaceVersion < 3 || positionMs != 0) { + bundle.putLong(FIELD_POSITION_MS, positionMs); + } + if (controllerInterfaceVersion < 3 || contentPositionMs != 0) { + bundle.putLong(FIELD_CONTENT_POSITION_MS, contentPositionMs); + } + if (adGroupIndex != C.INDEX_UNSET) { + bundle.putInt(FIELD_AD_GROUP_INDEX, adGroupIndex); + } + if (adIndexInAdGroup != C.INDEX_UNSET) { + bundle.putInt(FIELD_AD_INDEX_IN_AD_GROUP, adIndexInAdGroup); + } + return bundle; + } + /** * {@inheritDoc} * @@ -394,32 +470,7 @@ public interface Player { @UnstableApi @Override public Bundle toBundle() { - return toBundle(/* canAccessCurrentMediaItem= */ true, /* canAccessTimeline= */ true); - } - - /** - * Returns a {@link Bundle} representing the information stored in this object, filtered by - * available commands. - * - * @param canAccessCurrentMediaItem Whether the {@link Bundle} should contain information - * accessbile with {@link #COMMAND_GET_CURRENT_MEDIA_ITEM}. - * @param canAccessTimeline Whether the {@link Bundle} should contain information accessbile - * with {@link #COMMAND_GET_TIMELINE}. - */ - @UnstableApi - public Bundle toBundle(boolean canAccessCurrentMediaItem, boolean canAccessTimeline) { - Bundle bundle = new Bundle(); - bundle.putInt(FIELD_MEDIA_ITEM_INDEX, canAccessTimeline ? mediaItemIndex : 0); - if (mediaItem != null && canAccessCurrentMediaItem) { - bundle.putBundle(FIELD_MEDIA_ITEM, mediaItem.toBundle()); - } - bundle.putInt(FIELD_PERIOD_INDEX, canAccessTimeline ? periodIndex : 0); - bundle.putLong(FIELD_POSITION_MS, canAccessCurrentMediaItem ? positionMs : 0); - bundle.putLong(FIELD_CONTENT_POSITION_MS, canAccessCurrentMediaItem ? contentPositionMs : 0); - bundle.putInt(FIELD_AD_GROUP_INDEX, canAccessCurrentMediaItem ? adGroupIndex : C.INDEX_UNSET); - bundle.putInt( - FIELD_AD_INDEX_IN_AD_GROUP, canAccessCurrentMediaItem ? adIndexInAdGroup : C.INDEX_UNSET); - return bundle; + return toBundle(Integer.MAX_VALUE); } /** Object that can restore {@link PositionInfo} from a {@link Bundle}. */ @@ -3320,7 +3371,7 @@ public interface Player { * remote device is returned. * *

Note that this method returns the volume of the device. To check the current stream volume, - * use {@link getVolume()}. + * use {@link #getVolume()}. * *

This method must only be called if {@link #COMMAND_GET_DEVICE_VOLUME} is {@linkplain * #getAvailableCommands() available}. diff --git a/libraries/common/src/main/java/androidx/media3/common/Timeline.java b/libraries/common/src/main/java/androidx/media3/common/Timeline.java index d92f586ccc..4b338ee44b 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Timeline.java +++ b/libraries/common/src/main/java/androidx/media3/common/Timeline.java @@ -1443,36 +1443,29 @@ public abstract class Timeline implements Bundleable { } /** - * Returns a {@link Bundle} containing just the specified {@link Window}. + * Returns a copy of this timeline containing just the single specified {@link Window}. * - *

The {@link #getWindow(int, Window)} windows} and {@link #getPeriod(int, Period) periods} of - * an instance restored by {@link #CREATOR} may have missing fields as described in {@link - * Window#toBundle()} and {@link Period#toBundle()}. + *

The method returns the same instance if there is only one window. * - * @param windowIndex The index of the {@link Window} to include in the {@link Bundle}. + * @param windowIndex The index of the {@link Window} to include in the copy. + * @return A {@link Timeline} with just the single specified {@link Window}. */ @UnstableApi - public final Bundle toBundleWithOneWindowOnly(int windowIndex) { - Window window = getWindow(windowIndex, new Window(), /* defaultPositionProjectionUs= */ 0); - - List periodBundles = new ArrayList<>(); - Period period = new Period(); - for (int i = window.firstPeriodIndex; i <= window.lastPeriodIndex; i++) { - getPeriod(i, period, /* setIds= */ false); - period.windowIndex = 0; - periodBundles.add(period.toBundle()); + public final Timeline copyWithSingleWindow(int windowIndex) { + if (getWindowCount() == 1) { + return this; + } + Window window = getWindow(windowIndex, new Window(), /* defaultPositionProjectionUs= */ 0); + ImmutableList.Builder periods = ImmutableList.builder(); + for (int i = window.firstPeriodIndex; i <= window.lastPeriodIndex; i++) { + Period period = getPeriod(i, new Period(), /* setIds= */ true); + period.windowIndex = 0; + periods.add(period); } - window.lastPeriodIndex = window.lastPeriodIndex - window.firstPeriodIndex; window.firstPeriodIndex = 0; - Bundle windowBundle = window.toBundle(); - - Bundle bundle = new Bundle(); - BundleUtil.putBinder( - bundle, FIELD_WINDOWS, new BundleListRetriever(ImmutableList.of(windowBundle))); - BundleUtil.putBinder(bundle, FIELD_PERIODS, new BundleListRetriever(periodBundles)); - bundle.putIntArray(FIELD_SHUFFLED_WINDOW_INDICES, new int[] {0}); - return bundle; + return new RemotableTimeline( + ImmutableList.of(window), periods.build(), /* shuffledWindowIndices= */ new int[] {0}); } /** diff --git a/libraries/common/src/test/java/androidx/media3/common/PositionInfoTest.java b/libraries/common/src/test/java/androidx/media3/common/PositionInfoTest.java index 4f15d95c41..e4ffa25732 100644 --- a/libraries/common/src/test/java/androidx/media3/common/PositionInfoTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/PositionInfoTest.java @@ -17,6 +17,7 @@ package androidx.media3.common; import static com.google.common.truth.Truth.assertThat; +import android.os.Bundle; import androidx.media3.common.Player.PositionInfo; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; @@ -31,7 +32,7 @@ public class PositionInfoTest { PositionInfo positionInfo = new PositionInfo( /* windowUid= */ null, - /* windowIndex= */ 23, + /* mediaItemIndex= */ 23, new MediaItem.Builder().setMediaId("1234").build(), /* periodUid= */ null, /* periodIndex= */ 11, @@ -48,7 +49,7 @@ public class PositionInfoTest { PositionInfo positionInfo = new PositionInfo( /* windowUid= */ new Object(), - /* windowIndex= */ 23, + /* mediaItemIndex= */ 23, MediaItem.fromUri("https://exoplayer.dev"), /* periodUid= */ null, /* periodIndex= */ 11, @@ -66,7 +67,7 @@ public class PositionInfoTest { PositionInfo positionInfo = new PositionInfo( /* windowUid= */ null, - /* windowIndex= */ 23, + /* mediaItemIndex= */ 23, MediaItem.fromUri("https://exoplayer.dev"), /* periodUid= */ new Object(), /* periodIndex= */ 11, @@ -78,4 +79,69 @@ public class PositionInfoTest { PositionInfo positionInfoFromBundle = PositionInfo.CREATOR.fromBundle(positionInfo.toBundle()); assertThat(positionInfoFromBundle.periodUid).isNull(); } + + @Test + public void roundTripViaBundle_withDefaultValues_yieldsEqualInstance() { + PositionInfo defaultPositionInfo = + new PositionInfo( + /* windowUid= */ null, + /* mediaItemIndex= */ 0, + /* mediaItem= */ null, + /* periodUid= */ null, + /* periodIndex= */ 0, + /* positionMs= */ 0, + /* contentPositionMs= */ 0, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET); + + PositionInfo roundTripValue = PositionInfo.CREATOR.fromBundle(defaultPositionInfo.toBundle()); + + assertThat(roundTripValue).isEqualTo(defaultPositionInfo); + } + + @Test + public void toBundle_withDefaultValues_omitsAllData() { + PositionInfo defaultPositionInfo = + new PositionInfo( + /* windowUid= */ null, + /* mediaItemIndex= */ 0, + /* mediaItem= */ null, + /* periodUid= */ null, + /* periodIndex= */ 0, + /* positionMs= */ 0, + /* contentPositionMs= */ 0, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET); + + Bundle bundle = + defaultPositionInfo.toBundle(/* controllerInterfaceVersion= */ Integer.MAX_VALUE); + + assertThat(bundle.isEmpty()).isTrue(); + } + + @Test + public void toBundle_withDefaultValuesForControllerInterfaceBefore3_includesDefaultValues() { + // Controller before version 3 uses invalid default values for indices and the Bundle should + // always include them to avoid using the default values in the controller code. + PositionInfo defaultPositionInfo = + new PositionInfo( + /* windowUid= */ null, + /* mediaItemIndex= */ 0, + /* mediaItem= */ null, + /* periodUid= */ null, + /* periodIndex= */ 0, + /* positionMs= */ 0, + /* contentPositionMs= */ 0, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET); + + Bundle bundle = defaultPositionInfo.toBundle(/* controllerInterfaceVersion= */ 2); + + assertThat(bundle.keySet()) + .containsAtLeast( + PositionInfo.FIELD_MEDIA_ITEM_INDEX, + PositionInfo.FIELD_CONTENT_POSITION_MS, + PositionInfo.FIELD_PERIOD_INDEX, + PositionInfo.FIELD_POSITION_MS); + } } 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 450a62d0e6..bae2b9647d 100644 --- a/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java +++ b/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java @@ -98,6 +98,10 @@ import java.util.List; @Override public Bundle toBundle() { + return toBundle(Integer.MAX_VALUE); + } + + public Bundle toBundle(int controllerInterfaceVersion) { Bundle bundle = new Bundle(); bundle.putInt(FIELD_LIBRARY_VERSION, libraryVersion); BundleCompat.putBinder(bundle, FIELD_SESSION_BINDER, sessionBinder.asBinder()); @@ -114,8 +118,10 @@ import java.util.List; MediaUtils.intersect(playerCommandsFromSession, playerCommandsFromPlayer); bundle.putBundle( FIELD_PLAYER_INFO, - playerInfo.toBundle( - intersectedCommands, /* excludeTimeline= */ false, /* excludeTracks= */ false)); + playerInfo + .filterByAvailableCommands( + intersectedCommands, /* excludeTimeline= */ false, /* excludeTracks= */ false) + .toBundle(controllerInterfaceVersion)); bundle.putInt(FIELD_SESSION_INTERFACE_VERSION, sessionInterfaceVersion); return bundle; } 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 be8e4e877d..d4a8009856 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -1668,7 +1668,8 @@ public class MediaSession { int seq, SessionPositionInfo sessionPositionInfo, boolean canAccessCurrentMediaItem, - boolean canAccessTimeline) + boolean canAccessTimeline, + int controllerInterfaceVersion) throws RemoteException {} // Mostly matched with MediaController.ControllerCallback 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 29efaaa2ab..5a9939bdc6 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -932,7 +932,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; controller, (controllerCb, seq) -> controllerCb.onPeriodicSessionPositionInfoChanged( - seq, sessionPositionInfo, canAccessCurrentMediaItem, canAccessTimeline)); + seq, + sessionPositionInfo, + canAccessCurrentMediaItem, + canAccessTimeline, + controller.getInterfaceVersion())); } try { sessionLegacyStub @@ -941,7 +945,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /* seq= */ 0, sessionPositionInfo, /* canAccessCurrentMediaItem= */ true, - /* canAccessTimeline= */ true); + /* canAccessTimeline= */ true, + ControllerInfo.LEGACY_CONTROLLER_INTERFACE_VERSION); } catch (RemoteException e) { Log.e(TAG, "Exception in using media1 API", e); } 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 2e8b6ef4af..d9e177dc8a 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -1331,7 +1331,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; int unusedSeq, SessionPositionInfo unusedSessionPositionInfo, boolean unusedCanAccessCurrentMediaItem, - boolean unusedCanAccessTimeline) + boolean unusedCanAccessTimeline, + int controllerInterfaceVersion) throws RemoteException { updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper()); } 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 6b900a99c6..6f449112c2 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -117,6 +117,12 @@ import java.util.concurrent.ExecutionException; /** The version of the IMediaSession interface. */ public static final int VERSION_INT = 2; + /** + * Sequence number used when a controller method is triggered on the sesison side that wasn't + * initiated by the controller itself. + */ + public static final int UNKNOWN_SEQUENCE_NUMBER = Integer.MIN_VALUE; + private final WeakReference sessionImpl; private final MediaSessionManager sessionManager; private final ConnectedControllersManager connectedControllersManager; @@ -285,6 +291,18 @@ import java.util.concurrent.ExecutionException; int sequenceNumber, @Player.Command int command, SessionTask, K> task) { + ControllerInfo controllerInfo = connectedControllersManager.getController(caller.asBinder()); + if (controllerInfo != null) { + queueSessionTaskWithPlayerCommandForControllerInfo( + controllerInfo, sequenceNumber, command, task); + } + } + + private void queueSessionTaskWithPlayerCommandForControllerInfo( + ControllerInfo controller, + int sequenceNumber, + @Player.Command int command, + SessionTask, K> task) { long token = Binder.clearCallingIdentity(); try { @SuppressWarnings({"unchecked", "cast.unsafe"}) @@ -293,11 +311,6 @@ import java.util.concurrent.ExecutionException; if (sessionImpl == null || sessionImpl.isReleased()) { return; } - @Nullable - ControllerInfo controller = connectedControllersManager.getController(caller.asBinder()); - if (controller == null) { - return; - } postOrRun( sessionImpl.getApplicationHandler(), () -> { @@ -524,7 +537,10 @@ import java.util.concurrent.ExecutionException; } try { caller.onConnected( - sequencedFutureManager.obtainNextSequenceNumber(), state.toBundle()); + sequencedFutureManager.obtainNextSequenceNumber(), + caller instanceof MediaControllerStub + ? state.toBundleInProcess() + : state.toBundle(controllerInfo.getInterfaceVersion())); connected = true; } catch (RemoteException e) { // Controller may be died prematurely. @@ -618,8 +634,19 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - queueSessionTaskWithPlayerCommand( - caller, sequenceNumber, COMMAND_STOP, sendSessionResultSuccess(player -> player.stop())); + @Nullable + ControllerInfo controllerInfo = connectedControllersManager.getController(caller.asBinder()); + if (controllerInfo != null) { + stopForControllerInfo(controllerInfo, sequenceNumber); + } + } + + public void stopForControllerInfo(ControllerInfo controllerInfo, int sequenceNumber) { + queueSessionTaskWithPlayerCommandForControllerInfo( + controllerInfo, + sequenceNumber, + COMMAND_STOP, + sendSessionResultSuccess(PlayerWrapper::stop)); } @Override @@ -674,27 +701,30 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } + @Nullable ControllerInfo controller = connectedControllersManager.getController(caller.asBinder()); - if (controller == null) { - return; + if (controller != null) { + playForControllerInfo(controller, sequenceNumber); } - queueSessionTaskWithPlayerCommand( - caller, + } + + public void playForControllerInfo(ControllerInfo controller, int sequenceNumber) { + queueSessionTaskWithPlayerCommandForControllerInfo( + controller, sequenceNumber, COMMAND_PLAY_PAUSE, sendSessionResultSuccess( player -> { - @Nullable MediaSessionImpl sessionImpl = this.sessionImpl.get(); - if (sessionImpl == null || sessionImpl.isReleased()) { + @Nullable MediaSessionImpl impl = sessionImpl.get(); + if (impl == null || impl.isReleased()) { return; } - if (sessionImpl.onPlayRequested()) { + if (impl.onPlayRequested()) { if (player.getMediaItemCount() == 0) { // The player is in IDLE or ENDED state and has no media items in the playlist - // yet. - // Handle the play command as a playback resumption command to try resume + // yet. Handle the play command as a playback resumption command to try resume // playback. - sessionImpl.prepareAndPlayForPlaybackResumption(controller, player); + impl.prepareAndPlayForPlaybackResumption(controller, player); } else { Util.handlePlayButtonAction(player); } @@ -707,8 +737,16 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - queueSessionTaskWithPlayerCommand( - caller, sequenceNumber, COMMAND_PLAY_PAUSE, sendSessionResultSuccess(Player::pause)); + @Nullable + ControllerInfo controllerInfo = connectedControllersManager.getController(caller.asBinder()); + if (controllerInfo != null) { + pauseForControllerInfo(controllerInfo, sequenceNumber); + } + } + + public void pauseForControllerInfo(ControllerInfo controller, int sequenceNumber) { + queueSessionTaskWithPlayerCommandForControllerInfo( + controller, sequenceNumber, COMMAND_PLAY_PAUSE, sendSessionResultSuccess(Player::pause)); } @Override @@ -784,8 +822,19 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - queueSessionTaskWithPlayerCommand( - caller, sequenceNumber, COMMAND_SEEK_BACK, sendSessionResultSuccess(Player::seekBack)); + @Nullable + ControllerInfo controllerInfo = connectedControllersManager.getController(caller.asBinder()); + if (controllerInfo != null) { + seekBackForControllerInfo(controllerInfo, sequenceNumber); + } + } + + public void seekBackForControllerInfo(ControllerInfo controllerInfo, int sequenceNumber) { + queueSessionTaskWithPlayerCommandForControllerInfo( + controllerInfo, + sequenceNumber, + COMMAND_SEEK_BACK, + sendSessionResultSuccess(Player::seekBack)); } @Override @@ -793,8 +842,16 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - queueSessionTaskWithPlayerCommand( - caller, + @Nullable + ControllerInfo controllerInfo = connectedControllersManager.getController(caller.asBinder()); + if (controllerInfo != null) { + seekForwardForControllerInfo(controllerInfo, sequenceNumber); + } + } + + public void seekForwardForControllerInfo(ControllerInfo controllerInfo, int sequenceNumber) { + queueSessionTaskWithPlayerCommandForControllerInfo( + controllerInfo, sequenceNumber, COMMAND_SEEK_FORWARD, sendSessionResultSuccess(Player::seekForward)); @@ -1362,8 +1419,16 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - queueSessionTaskWithPlayerCommand( - caller, + @Nullable + ControllerInfo controllerInfo = connectedControllersManager.getController(caller.asBinder()); + if (controllerInfo != null) { + seekToPreviousForControllerInfo(controllerInfo, sequenceNumber); + } + } + + public void seekToPreviousForControllerInfo(ControllerInfo controllerInfo, int sequenceNumber) { + queueSessionTaskWithPlayerCommandForControllerInfo( + controllerInfo, sequenceNumber, COMMAND_SEEK_TO_PREVIOUS, sendSessionResultSuccess(Player::seekToPrevious)); @@ -1374,8 +1439,19 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - queueSessionTaskWithPlayerCommand( - caller, sequenceNumber, COMMAND_SEEK_TO_NEXT, sendSessionResultSuccess(Player::seekToNext)); + @Nullable + ControllerInfo controllerInfo = connectedControllersManager.getController(caller.asBinder()); + if (controllerInfo != null) { + seekToNextForControllerInfo(controllerInfo, sequenceNumber); + } + } + + public void seekToNextForControllerInfo(ControllerInfo controllerInfo, int sequenceNumber) { + queueSessionTaskWithPlayerCommandForControllerInfo( + controllerInfo, + sequenceNumber, + COMMAND_SEEK_TO_NEXT, + sendSessionResultSuccess(Player::seekToNext)); } @Override @@ -1913,16 +1989,25 @@ import java.util.concurrent.ExecutionException; boolean bundlingExclusionsTracks = excludeTracks || !availableCommands.contains(Player.COMMAND_GET_TRACKS); if (controllerInterfaceVersion >= 2) { + PlayerInfo filteredPlayerInfo = + playerInfo.filterByAvailableCommands(availableCommands, excludeTimeline, excludeTracks); + Bundle playerInfoBundle = + iController instanceof MediaControllerStub + ? filteredPlayerInfo.toBundleInProcess() + : filteredPlayerInfo.toBundle(controllerInterfaceVersion); iController.onPlayerInfoChangedWithExclusions( sequenceNumber, - playerInfo.toBundle(availableCommands, excludeTimeline, excludeTracks), + playerInfoBundle, new PlayerInfo.BundlingExclusions(bundlingExclusionsTimeline, bundlingExclusionsTracks) .toBundle()); } else { + PlayerInfo filteredPlayerInfo = + playerInfo.filterByAvailableCommands( + availableCommands, excludeTimeline, /* excludeTracks= */ true); //noinspection deprecation iController.onPlayerInfoChanged( sequenceNumber, - playerInfo.toBundle(availableCommands, excludeTimeline, /* excludeTracks= */ true), + filteredPlayerInfo.toBundle(controllerInterfaceVersion), bundlingExclusionsTimeline); } } @@ -1988,11 +2073,14 @@ import java.util.concurrent.ExecutionException; int sequenceNumber, SessionPositionInfo sessionPositionInfo, boolean canAccessCurrentMediaItem, - boolean canAccessTimeline) + boolean canAccessTimeline, + int controllerInterfaceVersion) throws RemoteException { iController.onPeriodicSessionPositionInfoChanged( sequenceNumber, - sessionPositionInfo.toBundle(canAccessCurrentMediaItem, canAccessTimeline)); + sessionPositionInfo + .filterByAvailableCommands(canAccessCurrentMediaItem, canAccessTimeline) + .toBundle(controllerInterfaceVersion)); } @Override 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 00b02e3e08..d7f6df30e7 100644 --- a/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java +++ b/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java @@ -22,10 +22,13 @@ import static androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_USER_R import static androidx.media3.common.Player.STATE_IDLE; import static androidx.media3.common.Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED; +import android.os.Binder; import android.os.Bundle; +import android.os.IBinder; import androidx.annotation.CheckResult; import androidx.annotation.FloatRange; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.media3.common.AudioAttributes; import androidx.media3.common.Bundleable; import androidx.media3.common.DeviceInfo; @@ -44,6 +47,7 @@ import androidx.media3.common.Tracks; import androidx.media3.common.VideoSize; import androidx.media3.common.text.CueGroup; import androidx.media3.common.util.Assertions; +import androidx.media3.common.util.BundleUtil; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import com.google.common.base.Objects; @@ -811,10 +815,10 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; private static final String FIELD_IS_PLAYING = Util.intToStringMaxRadix(16); private static final String FIELD_IS_LOADING = Util.intToStringMaxRadix(17); private static final String FIELD_PLAYBACK_ERROR = Util.intToStringMaxRadix(18); - private static final String FIELD_SESSION_POSITION_INFO = Util.intToStringMaxRadix(19); + @VisibleForTesting static final String FIELD_SESSION_POSITION_INFO = Util.intToStringMaxRadix(19); private static final String FIELD_MEDIA_ITEM_TRANSITION_REASON = Util.intToStringMaxRadix(20); - private static final String FIELD_OLD_POSITION_INFO = Util.intToStringMaxRadix(21); - private static final String FIELD_NEW_POSITION_INFO = Util.intToStringMaxRadix(22); + @VisibleForTesting static final String FIELD_OLD_POSITION_INFO = Util.intToStringMaxRadix(21); + @VisibleForTesting static final String FIELD_NEW_POSITION_INFO = Util.intToStringMaxRadix(22); private static final String FIELD_DISCONTINUITY_REASON = Util.intToStringMaxRadix(23); private static final String FIELD_CUE_GROUP = Util.intToStringMaxRadix(24); private static final String FIELD_MEDIA_METADATA = Util.intToStringMaxRadix(25); @@ -824,94 +828,198 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; private static final String FIELD_TRACK_SELECTION_PARAMETERS = Util.intToStringMaxRadix(29); private static final String FIELD_CURRENT_TRACKS = Util.intToStringMaxRadix(30); private static final String FIELD_TIMELINE_CHANGE_REASON = Util.intToStringMaxRadix(31); + private static final String FIELD_IN_PROCESS_BINDER = Util.intToStringMaxRadix(32); - // Next field key = 32 + // Next field key = 33 - public Bundle toBundle( + /** + * Returns a copy of this player info, filtered by the specified available commands. + * + *

The filtered fields are reset to their default values. + * + * @param availableCommands The available {@link Player.Commands} used to filter values. + * @param excludeTimeline Whether to filter the {@link #timeline} even if {@link + * Player#COMMAND_GET_TIMELINE} is available. + * @param excludeTracks Whether to filter the {@link #currentTracks} even if {@link + * Player#COMMAND_GET_TRACKS} is available. + * @return The filtered player info. + */ + public PlayerInfo filterByAvailableCommands( Player.Commands availableCommands, boolean excludeTimeline, boolean excludeTracks) { - Bundle bundle = new Bundle(); + PlayerInfo.Builder builder = new Builder(this); boolean canAccessCurrentMediaItem = availableCommands.contains(Player.COMMAND_GET_CURRENT_MEDIA_ITEM); boolean canAccessTimeline = availableCommands.contains(Player.COMMAND_GET_TIMELINE); - if (playerError != null) { - bundle.putBundle(FIELD_PLAYBACK_ERROR, playerError.toBundle()); + builder.setSessionPositionInfo( + sessionPositionInfo.filterByAvailableCommands( + canAccessCurrentMediaItem, canAccessTimeline)); + builder.setOldPositionInfo( + oldPositionInfo.filterByAvailableCommands(canAccessCurrentMediaItem, canAccessTimeline)); + builder.setNewPositionInfo( + newPositionInfo.filterByAvailableCommands(canAccessCurrentMediaItem, canAccessTimeline)); + if (!canAccessTimeline && canAccessCurrentMediaItem && !timeline.isEmpty()) { + builder.setTimeline( + timeline.copyWithSingleWindow(sessionPositionInfo.positionInfo.mediaItemIndex)); + } else if (excludeTimeline || !canAccessTimeline) { + builder.setTimeline(Timeline.EMPTY); } - bundle.putInt(FIELD_MEDIA_ITEM_TRANSITION_REASON, mediaItemTransitionReason); - bundle.putBundle( - FIELD_SESSION_POSITION_INFO, - sessionPositionInfo.toBundle(canAccessCurrentMediaItem, canAccessTimeline)); - bundle.putBundle( - FIELD_OLD_POSITION_INFO, - oldPositionInfo.toBundle(canAccessCurrentMediaItem, canAccessTimeline)); - bundle.putBundle( - FIELD_NEW_POSITION_INFO, - newPositionInfo.toBundle(canAccessCurrentMediaItem, canAccessTimeline)); - bundle.putInt(FIELD_DISCONTINUITY_REASON, discontinuityReason); - bundle.putBundle(FIELD_PLAYBACK_PARAMETERS, playbackParameters.toBundle()); - bundle.putInt(FIELD_REPEAT_MODE, repeatMode); - bundle.putBoolean(FIELD_SHUFFLE_MODE_ENABLED, shuffleModeEnabled); - if (!excludeTimeline && canAccessTimeline) { - bundle.putBundle(FIELD_TIMELINE, timeline.toBundle()); - } else if (!canAccessTimeline && canAccessCurrentMediaItem && !timeline.isEmpty()) { - bundle.putBundle( - FIELD_TIMELINE, - timeline.toBundleWithOneWindowOnly(sessionPositionInfo.positionInfo.mediaItemIndex)); + if (!availableCommands.contains(Player.COMMAND_GET_METADATA)) { + builder.setPlaylistMetadata(MediaMetadata.EMPTY); } - bundle.putInt(FIELD_TIMELINE_CHANGE_REASON, timelineChangeReason); - bundle.putBundle(FIELD_VIDEO_SIZE, videoSize.toBundle()); - if (availableCommands.contains(Player.COMMAND_GET_METADATA)) { - bundle.putBundle(FIELD_PLAYLIST_METADATA, playlistMetadata.toBundle()); + if (!availableCommands.contains(Player.COMMAND_GET_VOLUME)) { + builder.setVolume(1); } - if (availableCommands.contains(Player.COMMAND_GET_VOLUME)) { - bundle.putFloat(FIELD_VOLUME, volume); + if (!availableCommands.contains(Player.COMMAND_GET_AUDIO_ATTRIBUTES)) { + builder.setAudioAttributes(AudioAttributes.DEFAULT); } - if (availableCommands.contains(Player.COMMAND_GET_AUDIO_ATTRIBUTES)) { - bundle.putBundle(FIELD_AUDIO_ATTRIBUTES, audioAttributes.toBundle()); + if (!availableCommands.contains(Player.COMMAND_GET_TEXT)) { + builder.setCues(CueGroup.EMPTY_TIME_ZERO); } - if (availableCommands.contains(Player.COMMAND_GET_TEXT)) { - bundle.putBundle(FIELD_CUE_GROUP, cueGroup.toBundle()); + if (!availableCommands.contains(Player.COMMAND_GET_DEVICE_VOLUME)) { + builder.setDeviceVolume(0).setDeviceMuted(false); } - bundle.putBundle(FIELD_DEVICE_INFO, deviceInfo.toBundle()); - if (availableCommands.contains(Player.COMMAND_GET_DEVICE_VOLUME)) { - bundle.putInt(FIELD_DEVICE_VOLUME, deviceVolume); - bundle.putBoolean(FIELD_DEVICE_MUTED, deviceMuted); + if (!availableCommands.contains(Player.COMMAND_GET_METADATA)) { + builder.setMediaMetadata(MediaMetadata.EMPTY); } - bundle.putBoolean(FIELD_PLAY_WHEN_READY, playWhenReady); - bundle.putInt(FIELD_PLAYBACK_SUPPRESSION_REASON, playbackSuppressionReason); - bundle.putInt(FIELD_PLAYBACK_STATE, playbackState); - bundle.putBoolean(FIELD_IS_PLAYING, isPlaying); - bundle.putBoolean(FIELD_IS_LOADING, isLoading); - if (availableCommands.contains(Player.COMMAND_GET_METADATA)) { - bundle.putBundle(FIELD_MEDIA_METADATA, mediaMetadata.toBundle()); + if (excludeTracks || !availableCommands.contains(Player.COMMAND_GET_TRACKS)) { + builder.setCurrentTracks(Tracks.EMPTY); } - bundle.putLong(FIELD_SEEK_BACK_INCREMENT_MS, seekBackIncrementMs); - bundle.putLong(FIELD_SEEK_FORWARD_INCREMENT_MS, seekForwardIncrementMs); - bundle.putLong(FIELD_MAX_SEEK_TO_PREVIOUS_POSITION_MS, maxSeekToPreviousPositionMs); - if (!excludeTracks && availableCommands.contains(Player.COMMAND_GET_TRACKS)) { - bundle.putBundle(FIELD_CURRENT_TRACKS, currentTracks.toBundle()); - } - bundle.putBundle(FIELD_TRACK_SELECTION_PARAMETERS, trackSelectionParameters.toBundle()); + return builder.build(); + } + + /** + * Returns a {@link Bundle} that stores a direct object reference to this class for in-process + * sharing. + */ + public Bundle toBundleInProcess() { + Bundle bundle = new Bundle(); + BundleUtil.putBinder(bundle, FIELD_IN_PROCESS_BINDER, new InProcessBinder()); return bundle; } @Override public Bundle toBundle() { - return toBundle( - /* availableCommands= */ new Player.Commands.Builder().addAllCommands().build(), - /* excludeTimeline= */ false, - /* excludeTracks= */ false); + return toBundle(Integer.MAX_VALUE); + } + + public Bundle toBundle(int controllerInterfaceVersion) { + Bundle bundle = new Bundle(); + if (playerError != null) { + bundle.putBundle(FIELD_PLAYBACK_ERROR, playerError.toBundle()); + } + if (mediaItemTransitionReason != MEDIA_ITEM_TRANSITION_REASON_DEFAULT) { + bundle.putInt(FIELD_MEDIA_ITEM_TRANSITION_REASON, mediaItemTransitionReason); + } + if (controllerInterfaceVersion < 3 + || !sessionPositionInfo.equals(SessionPositionInfo.DEFAULT)) { + bundle.putBundle( + FIELD_SESSION_POSITION_INFO, sessionPositionInfo.toBundle(controllerInterfaceVersion)); + } + if (controllerInterfaceVersion < 3 + || !SessionPositionInfo.DEFAULT_POSITION_INFO.equalsForBundling(oldPositionInfo)) { + bundle.putBundle( + FIELD_OLD_POSITION_INFO, oldPositionInfo.toBundle(controllerInterfaceVersion)); + } + if (controllerInterfaceVersion < 3 + || !SessionPositionInfo.DEFAULT_POSITION_INFO.equalsForBundling(newPositionInfo)) { + bundle.putBundle( + FIELD_NEW_POSITION_INFO, newPositionInfo.toBundle(controllerInterfaceVersion)); + } + if (discontinuityReason != DISCONTINUITY_REASON_DEFAULT) { + bundle.putInt(FIELD_DISCONTINUITY_REASON, discontinuityReason); + } + if (!playbackParameters.equals(PlaybackParameters.DEFAULT)) { + bundle.putBundle(FIELD_PLAYBACK_PARAMETERS, playbackParameters.toBundle()); + } + if (repeatMode != Player.REPEAT_MODE_OFF) { + bundle.putInt(FIELD_REPEAT_MODE, repeatMode); + } + if (shuffleModeEnabled) { + bundle.putBoolean(FIELD_SHUFFLE_MODE_ENABLED, shuffleModeEnabled); + } + if (!timeline.equals(Timeline.EMPTY)) { + bundle.putBundle(FIELD_TIMELINE, timeline.toBundle()); + } + if (timelineChangeReason != TIMELINE_CHANGE_REASON_DEFAULT) { + bundle.putInt(FIELD_TIMELINE_CHANGE_REASON, timelineChangeReason); + } + if (!videoSize.equals(VideoSize.UNKNOWN)) { + bundle.putBundle(FIELD_VIDEO_SIZE, videoSize.toBundle()); + } + if (!playlistMetadata.equals(MediaMetadata.EMPTY)) { + bundle.putBundle(FIELD_PLAYLIST_METADATA, playlistMetadata.toBundle()); + } + if (volume != 1) { + bundle.putFloat(FIELD_VOLUME, volume); + } + if (!audioAttributes.equals(AudioAttributes.DEFAULT)) { + bundle.putBundle(FIELD_AUDIO_ATTRIBUTES, audioAttributes.toBundle()); + } + if (!cueGroup.equals(CueGroup.EMPTY_TIME_ZERO)) { + bundle.putBundle(FIELD_CUE_GROUP, cueGroup.toBundle()); + } + if (!deviceInfo.equals(DeviceInfo.UNKNOWN)) { + bundle.putBundle(FIELD_DEVICE_INFO, deviceInfo.toBundle()); + } + if (deviceVolume != 0) { + bundle.putInt(FIELD_DEVICE_VOLUME, deviceVolume); + } + if (deviceMuted) { + bundle.putBoolean(FIELD_DEVICE_MUTED, deviceMuted); + } + if (playWhenReady) { + bundle.putBoolean(FIELD_PLAY_WHEN_READY, playWhenReady); + } + if (playWhenReadyChangeReason != PLAY_WHEN_READY_CHANGE_REASON_DEFAULT) { + bundle.putInt(FIELD_PLAY_WHEN_READY_CHANGE_REASON, playWhenReadyChangeReason); + } + if (playbackSuppressionReason != PLAYBACK_SUPPRESSION_REASON_NONE) { + bundle.putInt(FIELD_PLAYBACK_SUPPRESSION_REASON, playbackSuppressionReason); + } + if (playbackState != STATE_IDLE) { + bundle.putInt(FIELD_PLAYBACK_STATE, playbackState); + } + if (isPlaying) { + bundle.putBoolean(FIELD_IS_PLAYING, isPlaying); + } + if (isLoading) { + bundle.putBoolean(FIELD_IS_LOADING, isLoading); + } + if (!mediaMetadata.equals(MediaMetadata.EMPTY)) { + bundle.putBundle(FIELD_MEDIA_METADATA, mediaMetadata.toBundle()); + } + if (seekBackIncrementMs != 0) { + bundle.putLong(FIELD_SEEK_BACK_INCREMENT_MS, seekBackIncrementMs); + } + if (seekForwardIncrementMs != 0) { + bundle.putLong(FIELD_SEEK_FORWARD_INCREMENT_MS, seekForwardIncrementMs); + } + if (maxSeekToPreviousPositionMs != 0) { + bundle.putLong(FIELD_MAX_SEEK_TO_PREVIOUS_POSITION_MS, maxSeekToPreviousPositionMs); + } + if (!currentTracks.equals(Tracks.EMPTY)) { + bundle.putBundle(FIELD_CURRENT_TRACKS, currentTracks.toBundle()); + } + if (!trackSelectionParameters.equals(TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT)) { + bundle.putBundle(FIELD_TRACK_SELECTION_PARAMETERS, trackSelectionParameters.toBundle()); + } + return bundle; } /** Object that can restore {@link PlayerInfo} from a {@link Bundle}. */ public static final Creator CREATOR = PlayerInfo::fromBundle; private static PlayerInfo fromBundle(Bundle bundle) { + @Nullable IBinder inProcessBinder = BundleUtil.getBinder(bundle, FIELD_IN_PROCESS_BINDER); + if (inProcessBinder instanceof InProcessBinder) { + return ((InProcessBinder) inProcessBinder).getPlayerInfo(); + } @Nullable Bundle playerErrorBundle = bundle.getBundle(FIELD_PLAYBACK_ERROR); @Nullable PlaybackException playerError = playerErrorBundle == null ? null : PlaybackException.CREATOR.fromBundle(playerErrorBundle); int mediaItemTransitionReason = - bundle.getInt(FIELD_MEDIA_ITEM_TRANSITION_REASON, MEDIA_ITEM_TRANSITION_REASON_REPEAT); + bundle.getInt(FIELD_MEDIA_ITEM_TRANSITION_REASON, MEDIA_ITEM_TRANSITION_REASON_DEFAULT); @Nullable Bundle sessionPositionInfoBundle = bundle.getBundle(FIELD_SESSION_POSITION_INFO); SessionPositionInfo sessionPositionInfo = sessionPositionInfoBundle == null @@ -928,7 +1036,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; ? SessionPositionInfo.DEFAULT_POSITION_INFO : PositionInfo.CREATOR.fromBundle(newPositionInfoBundle); int discontinuityReason = - bundle.getInt(FIELD_DISCONTINUITY_REASON, DISCONTINUITY_REASON_AUTO_TRANSITION); + bundle.getInt(FIELD_DISCONTINUITY_REASON, DISCONTINUITY_REASON_DEFAULT); @Nullable Bundle playbackParametersBundle = bundle.getBundle(FIELD_PLAYBACK_PARAMETERS); PlaybackParameters playbackParameters = playbackParametersBundle == null @@ -974,7 +1082,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; int playWhenReadyChangeReason = bundle.getInt( FIELD_PLAY_WHEN_READY_CHANGE_REASON, - /* defaultValue= */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); + /* defaultValue= */ PLAY_WHEN_READY_CHANGE_REASON_DEFAULT); @Player.PlaybackSuppressionReason int playbackSuppressionReason = bundle.getInt( @@ -1036,4 +1144,10 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; currentTracks, trackSelectionParameters); } + + private final class InProcessBinder extends Binder { + public PlayerInfo getPlayerInfo() { + return PlayerInfo.this; + } + } } diff --git a/libraries/session/src/main/java/androidx/media3/session/SessionPositionInfo.java b/libraries/session/src/main/java/androidx/media3/session/SessionPositionInfo.java index a4d537f0cc..02f5476cf6 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SessionPositionInfo.java +++ b/libraries/session/src/main/java/androidx/media3/session/SessionPositionInfo.java @@ -19,8 +19,10 @@ import static androidx.media3.common.util.Assertions.checkArgument; import android.os.Bundle; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.media3.common.Bundleable; import androidx.media3.common.C; +import androidx.media3.common.Player; import androidx.media3.common.Player.PositionInfo; import androidx.media3.common.util.Util; import com.google.common.base.Objects; @@ -101,9 +103,9 @@ import com.google.common.base.Objects; return false; } SessionPositionInfo other = (SessionPositionInfo) obj; - return positionInfo.equals(other.positionInfo) + return eventTimeMs == other.eventTimeMs + && positionInfo.equals(other.positionInfo) && isPlayingAd == other.isPlayingAd - && eventTimeMs == other.eventTimeMs && durationMs == other.durationMs && bufferedPositionMs == other.bufferedPositionMs && bufferedPercentage == other.bufferedPercentage @@ -157,41 +159,86 @@ import com.google.common.base.Objects; // Bundleable implementation. - private static final String FIELD_POSITION_INFO = Util.intToStringMaxRadix(0); + @VisibleForTesting static final String FIELD_POSITION_INFO = Util.intToStringMaxRadix(0); private static final String FIELD_IS_PLAYING_AD = Util.intToStringMaxRadix(1); private static final String FIELD_EVENT_TIME_MS = Util.intToStringMaxRadix(2); private static final String FIELD_DURATION_MS = Util.intToStringMaxRadix(3); - private static final String FIELD_BUFFERED_POSITION_MS = Util.intToStringMaxRadix(4); + @VisibleForTesting static final String FIELD_BUFFERED_POSITION_MS = Util.intToStringMaxRadix(4); private static final String FIELD_BUFFERED_PERCENTAGE = Util.intToStringMaxRadix(5); private static final String FIELD_TOTAL_BUFFERED_DURATION_MS = Util.intToStringMaxRadix(6); private static final String FIELD_CURRENT_LIVE_OFFSET_MS = Util.intToStringMaxRadix(7); private static final String FIELD_CONTENT_DURATION_MS = Util.intToStringMaxRadix(8); - private static final String FIELD_CONTENT_BUFFERED_POSITION_MS = Util.intToStringMaxRadix(9); + + @VisibleForTesting + static final String FIELD_CONTENT_BUFFERED_POSITION_MS = Util.intToStringMaxRadix(9); + + /** + * Returns a copy of this session position info, filtered by the specified available commands. + * + *

The filtered fields are reset to their default values. + * + *

The return value may be the same object if nothing is filtered. + * + * @param canAccessCurrentMediaItem Whether {@link Player#COMMAND_GET_CURRENT_MEDIA_ITEM} is + * available. + * @param canAccessTimeline Whether {@link Player#COMMAND_GET_TIMELINE} is available. + * @return The filtered session position info. + */ + public SessionPositionInfo filterByAvailableCommands( + boolean canAccessCurrentMediaItem, boolean canAccessTimeline) { + if (canAccessCurrentMediaItem && canAccessTimeline) { + return this; + } + return new SessionPositionInfo( + positionInfo.filterByAvailableCommands(canAccessCurrentMediaItem, canAccessTimeline), + canAccessCurrentMediaItem && isPlayingAd, + eventTimeMs, + canAccessCurrentMediaItem ? durationMs : C.TIME_UNSET, + canAccessCurrentMediaItem ? bufferedPositionMs : 0, + canAccessCurrentMediaItem ? bufferedPercentage : 0, + canAccessCurrentMediaItem ? totalBufferedDurationMs : 0, + canAccessCurrentMediaItem ? currentLiveOffsetMs : C.TIME_UNSET, + canAccessCurrentMediaItem ? contentDurationMs : C.TIME_UNSET, + canAccessCurrentMediaItem ? contentBufferedPositionMs : 0); + } @Override public Bundle toBundle() { - return toBundle(/* canAccessCurrentMediaItem= */ true, /* canAccessTimeline= */ true); + return toBundle(Integer.MAX_VALUE); } - public Bundle toBundle(boolean canAccessCurrentMediaItem, boolean canAccessTimeline) { + public Bundle toBundle(int controllerInterfaceVersion) { Bundle bundle = new Bundle(); - bundle.putBundle( - FIELD_POSITION_INFO, positionInfo.toBundle(canAccessCurrentMediaItem, canAccessTimeline)); - bundle.putBoolean(FIELD_IS_PLAYING_AD, canAccessCurrentMediaItem && isPlayingAd); - bundle.putLong(FIELD_EVENT_TIME_MS, eventTimeMs); - bundle.putLong(FIELD_DURATION_MS, canAccessCurrentMediaItem ? durationMs : C.TIME_UNSET); - bundle.putLong(FIELD_BUFFERED_POSITION_MS, canAccessCurrentMediaItem ? bufferedPositionMs : 0); - bundle.putInt(FIELD_BUFFERED_PERCENTAGE, canAccessCurrentMediaItem ? bufferedPercentage : 0); - bundle.putLong( - FIELD_TOTAL_BUFFERED_DURATION_MS, canAccessCurrentMediaItem ? totalBufferedDurationMs : 0); - bundle.putLong( - FIELD_CURRENT_LIVE_OFFSET_MS, - canAccessCurrentMediaItem ? currentLiveOffsetMs : C.TIME_UNSET); - bundle.putLong( - FIELD_CONTENT_DURATION_MS, canAccessCurrentMediaItem ? contentDurationMs : C.TIME_UNSET); - bundle.putLong( - FIELD_CONTENT_BUFFERED_POSITION_MS, - canAccessCurrentMediaItem ? contentBufferedPositionMs : 0); + if (controllerInterfaceVersion < 3 || !DEFAULT_POSITION_INFO.equalsForBundling(positionInfo)) { + bundle.putBundle(FIELD_POSITION_INFO, positionInfo.toBundle(controllerInterfaceVersion)); + } + if (isPlayingAd) { + bundle.putBoolean(FIELD_IS_PLAYING_AD, isPlayingAd); + } + if (eventTimeMs != C.TIME_UNSET) { + bundle.putLong(FIELD_EVENT_TIME_MS, eventTimeMs); + } + if (durationMs != C.TIME_UNSET) { + bundle.putLong(FIELD_DURATION_MS, durationMs); + } + if (controllerInterfaceVersion < 3 || bufferedPositionMs != 0) { + bundle.putLong(FIELD_BUFFERED_POSITION_MS, bufferedPositionMs); + } + if (bufferedPercentage != 0) { + bundle.putInt(FIELD_BUFFERED_PERCENTAGE, bufferedPercentage); + } + if (totalBufferedDurationMs != 0) { + bundle.putLong(FIELD_TOTAL_BUFFERED_DURATION_MS, totalBufferedDurationMs); + } + if (currentLiveOffsetMs != C.TIME_UNSET) { + bundle.putLong(FIELD_CURRENT_LIVE_OFFSET_MS, currentLiveOffsetMs); + } + if (contentDurationMs != C.TIME_UNSET) { + bundle.putLong(FIELD_CONTENT_DURATION_MS, contentDurationMs); + } + if (controllerInterfaceVersion < 3 || contentBufferedPositionMs != 0) { + bundle.putLong(FIELD_CONTENT_BUFFERED_POSITION_MS, contentBufferedPositionMs); + } return bundle; } diff --git a/libraries/session/src/test/java/androidx/media3/session/PlayerInfoTest.java b/libraries/session/src/test/java/androidx/media3/session/PlayerInfoTest.java index c3e6736ed3..3ed7f51c61 100644 --- a/libraries/session/src/test/java/androidx/media3/session/PlayerInfoTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/PlayerInfoTest.java @@ -71,7 +71,7 @@ public class PlayerInfoTest { } @Test - public void toBundleFromBundle_withAllCommands_restoresAllData() { + public void toBundleFromBundle_restoresAllData() { PlayerInfo playerInfo = new PlayerInfo.Builder(PlayerInfo.DEFAULT) .setOldPositionInfo( @@ -151,7 +151,7 @@ public class PlayerInfoTest { new PlaybackException( /* message= */ null, /* cause= */ null, PlaybackException.ERROR_CODE_TIMEOUT)) .setPlayWhenReady(true) - .setPlayWhenReadyChangeReason(Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST) + .setPlayWhenReadyChangeReason(Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS) .setRepeatMode(Player.REPEAT_MODE_ONE) .setSeekBackIncrement(7000) .setSeekForwardIncrement(6000) @@ -163,12 +163,7 @@ public class PlayerInfoTest { .setVideoSize(new VideoSize(/* width= */ 1024, /* height= */ 768)) .build(); - PlayerInfo infoAfterBundling = - PlayerInfo.CREATOR.fromBundle( - playerInfo.toBundle( - new Player.Commands.Builder().addAllCommands().build(), - /* excludeTimeline= */ false, - /* excludeTracks= */ false)); + PlayerInfo infoAfterBundling = PlayerInfo.CREATOR.fromBundle(playerInfo.toBundle()); assertThat(infoAfterBundling.oldPositionInfo.mediaItemIndex).isEqualTo(5); assertThat(infoAfterBundling.oldPositionInfo.periodIndex).isEqualTo(4); @@ -205,8 +200,8 @@ public class PlayerInfoTest { assertThat(infoAfterBundling.timeline.getWindowCount()).isEqualTo(10); assertThat(infoAfterBundling.timelineChangeReason) .isEqualTo(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); - assertThat(infoAfterBundling.mediaMetadata.title).isEqualTo("title"); - assertThat(infoAfterBundling.playlistMetadata.artist).isEqualTo("artist"); + assertThat(infoAfterBundling.mediaMetadata.title.toString()).isEqualTo("title"); + assertThat(infoAfterBundling.playlistMetadata.artist.toString()).isEqualTo("artist"); assertThat(infoAfterBundling.volume).isEqualTo(0.5f); assertThat(infoAfterBundling.deviceVolume).isEqualTo(10); assertThat(infoAfterBundling.deviceMuted).isTrue(); @@ -229,7 +224,7 @@ public class PlayerInfoTest { .isEqualTo(PlaybackException.ERROR_CODE_TIMEOUT); assertThat(infoAfterBundling.playWhenReady).isTrue(); assertThat(infoAfterBundling.playWhenReadyChangeReason) - .isEqualTo(Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); + .isEqualTo(Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS); assertThat(infoAfterBundling.repeatMode).isEqualTo(Player.REPEAT_MODE_ONE); assertThat(infoAfterBundling.seekBackIncrementMs).isEqualTo(7000); assertThat(infoAfterBundling.seekForwardIncrementMs).isEqualTo(6000); @@ -289,13 +284,15 @@ public class PlayerInfoTest { PlayerInfo infoAfterBundling = PlayerInfo.CREATOR.fromBundle( - playerInfo.toBundle( - new Player.Commands.Builder() - .addAllCommands() - .remove(Player.COMMAND_GET_CURRENT_MEDIA_ITEM) - .build(), - /* excludeTimeline= */ false, - /* excludeTracks= */ false)); + playerInfo + .filterByAvailableCommands( + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_GET_CURRENT_MEDIA_ITEM) + .build(), + /* excludeTimeline= */ false, + /* excludeTracks= */ false) + .toBundle()); assertThat(infoAfterBundling.oldPositionInfo.mediaItemIndex).isEqualTo(5); assertThat(infoAfterBundling.oldPositionInfo.periodIndex).isEqualTo(4); @@ -408,13 +405,15 @@ public class PlayerInfoTest { PlayerInfo infoAfterBundling = PlayerInfo.CREATOR.fromBundle( - playerInfo.toBundle( - new Player.Commands.Builder() - .addAllCommands() - .remove(Player.COMMAND_GET_TIMELINE) - .build(), - /* excludeTimeline= */ true, - /* excludeTracks= */ false)); + playerInfo + .filterByAvailableCommands( + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_GET_TIMELINE) + .build(), + /* excludeTimeline= */ true, + /* excludeTracks= */ false) + .toBundle()); assertThat(infoAfterBundling.oldPositionInfo.mediaItemIndex).isEqualTo(0); assertThat(infoAfterBundling.oldPositionInfo.periodIndex).isEqualTo(0); @@ -475,13 +474,15 @@ public class PlayerInfoTest { PlayerInfo infoAfterBundling = PlayerInfo.CREATOR.fromBundle( - playerInfo.toBundle( - new Player.Commands.Builder() - .addAllCommands() - .remove(Player.COMMAND_GET_METADATA) - .build(), - /* excludeTimeline= */ false, - /* excludeTracks= */ false)); + playerInfo + .filterByAvailableCommands( + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_GET_METADATA) + .build(), + /* excludeTimeline= */ false, + /* excludeTracks= */ false) + .toBundle()); assertThat(infoAfterBundling.mediaMetadata).isEqualTo(MediaMetadata.EMPTY); assertThat(infoAfterBundling.playlistMetadata).isEqualTo(MediaMetadata.EMPTY); @@ -493,13 +494,15 @@ public class PlayerInfoTest { PlayerInfo infoAfterBundling = PlayerInfo.CREATOR.fromBundle( - playerInfo.toBundle( - new Player.Commands.Builder() - .addAllCommands() - .remove(Player.COMMAND_GET_VOLUME) - .build(), - /* excludeTimeline= */ false, - /* excludeTracks= */ false)); + playerInfo + .filterByAvailableCommands( + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_GET_VOLUME) + .build(), + /* excludeTimeline= */ false, + /* excludeTracks= */ false) + .toBundle()); assertThat(infoAfterBundling.volume).isEqualTo(1f); } @@ -511,13 +514,15 @@ public class PlayerInfoTest { PlayerInfo infoAfterBundling = PlayerInfo.CREATOR.fromBundle( - playerInfo.toBundle( - new Player.Commands.Builder() - .addAllCommands() - .remove(Player.COMMAND_GET_DEVICE_VOLUME) - .build(), - /* excludeTimeline= */ false, - /* excludeTracks= */ false)); + playerInfo + .filterByAvailableCommands( + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_GET_DEVICE_VOLUME) + .build(), + /* excludeTimeline= */ false, + /* excludeTracks= */ false) + .toBundle()); assertThat(infoAfterBundling.deviceVolume).isEqualTo(0); assertThat(infoAfterBundling.deviceMuted).isFalse(); @@ -533,13 +538,15 @@ public class PlayerInfoTest { PlayerInfo infoAfterBundling = PlayerInfo.CREATOR.fromBundle( - playerInfo.toBundle( - new Player.Commands.Builder() - .addAllCommands() - .remove(Player.COMMAND_GET_AUDIO_ATTRIBUTES) - .build(), - /* excludeTimeline= */ false, - /* excludeTracks= */ false)); + playerInfo + .filterByAvailableCommands( + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_GET_AUDIO_ATTRIBUTES) + .build(), + /* excludeTimeline= */ false, + /* excludeTracks= */ false) + .toBundle()); assertThat(infoAfterBundling.audioAttributes).isEqualTo(AudioAttributes.DEFAULT); } @@ -553,13 +560,15 @@ public class PlayerInfoTest { PlayerInfo infoAfterBundling = PlayerInfo.CREATOR.fromBundle( - playerInfo.toBundle( - new Player.Commands.Builder() - .addAllCommands() - .remove(Player.COMMAND_GET_TEXT) - .build(), - /* excludeTimeline= */ false, - /* excludeTracks= */ false)); + playerInfo + .filterByAvailableCommands( + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_GET_TEXT) + .build(), + /* excludeTimeline= */ false, + /* excludeTracks= */ false) + .toBundle()); assertThat(infoAfterBundling.cueGroup).isEqualTo(CueGroup.EMPTY_TIME_ZERO); } @@ -581,14 +590,84 @@ public class PlayerInfoTest { PlayerInfo infoAfterBundling = PlayerInfo.CREATOR.fromBundle( - playerInfo.toBundle( - new Player.Commands.Builder() - .addAllCommands() - .remove(Player.COMMAND_GET_TRACKS) - .build(), - /* excludeTimeline= */ false, - /* excludeTracks= */ true)); + playerInfo + .filterByAvailableCommands( + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_GET_TRACKS) + .build(), + /* excludeTimeline= */ false, + /* excludeTracks= */ true) + .toBundle()); assertThat(infoAfterBundling.currentTracks).isEqualTo(Tracks.EMPTY); } + + @Test + public void toBundleFromBundle_withDefaultValues_restoresAllData() { + PlayerInfo roundTripValue = PlayerInfo.CREATOR.fromBundle(PlayerInfo.DEFAULT.toBundle()); + + assertThat(roundTripValue.oldPositionInfo).isEqualTo(PlayerInfo.DEFAULT.oldPositionInfo); + assertThat(roundTripValue.newPositionInfo).isEqualTo(PlayerInfo.DEFAULT.newPositionInfo); + assertThat(roundTripValue.sessionPositionInfo) + .isEqualTo(PlayerInfo.DEFAULT.sessionPositionInfo); + assertThat(roundTripValue.timeline).isEqualTo(PlayerInfo.DEFAULT.timeline); + assertThat(roundTripValue.timelineChangeReason) + .isEqualTo(PlayerInfo.DEFAULT.timelineChangeReason); + assertThat(roundTripValue.mediaMetadata).isEqualTo(PlayerInfo.DEFAULT.mediaMetadata); + assertThat(roundTripValue.playlistMetadata).isEqualTo(PlayerInfo.DEFAULT.playlistMetadata); + assertThat(roundTripValue.volume).isEqualTo(PlayerInfo.DEFAULT.volume); + assertThat(roundTripValue.deviceVolume).isEqualTo(PlayerInfo.DEFAULT.deviceVolume); + assertThat(roundTripValue.deviceMuted).isEqualTo(PlayerInfo.DEFAULT.deviceMuted); + assertThat(roundTripValue.audioAttributes).isEqualTo(PlayerInfo.DEFAULT.audioAttributes); + assertThat(roundTripValue.cueGroup).isEqualTo(PlayerInfo.DEFAULT.cueGroup); + assertThat(roundTripValue.currentTracks).isEqualTo(PlayerInfo.DEFAULT.currentTracks); + assertThat(roundTripValue.deviceInfo).isEqualTo(PlayerInfo.DEFAULT.deviceInfo); + assertThat(roundTripValue.discontinuityReason) + .isEqualTo(PlayerInfo.DEFAULT.discontinuityReason); + assertThat(roundTripValue.isLoading).isEqualTo(PlayerInfo.DEFAULT.isLoading); + assertThat(roundTripValue.isPlaying).isEqualTo(PlayerInfo.DEFAULT.isPlaying); + assertThat(roundTripValue.maxSeekToPreviousPositionMs) + .isEqualTo(PlayerInfo.DEFAULT.maxSeekToPreviousPositionMs); + assertThat(roundTripValue.mediaItemTransitionReason) + .isEqualTo(PlayerInfo.DEFAULT.mediaItemTransitionReason); + assertThat(roundTripValue.playbackParameters).isEqualTo(PlayerInfo.DEFAULT.playbackParameters); + assertThat(roundTripValue.playbackState).isEqualTo(PlayerInfo.DEFAULT.playbackState); + assertThat(roundTripValue.playbackSuppressionReason) + .isEqualTo(PlayerInfo.DEFAULT.playbackSuppressionReason); + assertThat(roundTripValue.playerError).isEqualTo(PlayerInfo.DEFAULT.playerError); + assertThat(roundTripValue.playWhenReady).isEqualTo(PlayerInfo.DEFAULT.playWhenReady); + assertThat(roundTripValue.playWhenReadyChangeReason) + .isEqualTo(PlayerInfo.DEFAULT.playWhenReadyChangeReason); + assertThat(roundTripValue.repeatMode).isEqualTo(PlayerInfo.DEFAULT.repeatMode); + assertThat(roundTripValue.seekBackIncrementMs) + .isEqualTo(PlayerInfo.DEFAULT.seekBackIncrementMs); + assertThat(roundTripValue.seekForwardIncrementMs) + .isEqualTo(PlayerInfo.DEFAULT.seekForwardIncrementMs); + assertThat(roundTripValue.shuffleModeEnabled).isEqualTo(PlayerInfo.DEFAULT.shuffleModeEnabled); + assertThat(roundTripValue.trackSelectionParameters) + .isEqualTo(PlayerInfo.DEFAULT.trackSelectionParameters); + assertThat(roundTripValue.videoSize).isEqualTo(PlayerInfo.DEFAULT.videoSize); + } + + @Test + public void toBundle_withDefaultValues_omitsAllData() { + Bundle bundle = + PlayerInfo.DEFAULT.toBundle(/* controllerInterfaceVersion= */ Integer.MAX_VALUE); + + assertThat(bundle.isEmpty()).isTrue(); + } + + @Test + public void toBundle_withDefaultValuesForControllerInterfaceBefore3_includesPositionInfos() { + // Controller before version 3 uses invalid default values for indices in (Session)PositionInfo. + // The Bundle should always include these fields to avoid using the invalid defaults. + Bundle bundle = PlayerInfo.DEFAULT.toBundle(/* controllerInterfaceVersion= */ 2); + + assertThat(bundle.keySet()) + .containsAtLeast( + PlayerInfo.FIELD_SESSION_POSITION_INFO, + PlayerInfo.FIELD_NEW_POSITION_INFO, + PlayerInfo.FIELD_OLD_POSITION_INFO); + } } diff --git a/libraries/session/src/test/java/androidx/media3/session/SessionPositionInfoTest.java b/libraries/session/src/test/java/androidx/media3/session/SessionPositionInfoTest.java index 9ea9b236b3..96dc7e76ed 100644 --- a/libraries/session/src/test/java/androidx/media3/session/SessionPositionInfoTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/SessionPositionInfoTest.java @@ -85,4 +85,35 @@ public class SessionPositionInfoTest { /* contentDurationMs= */ 400L, /* contentBufferedPositionMs= */ 223L)); } + + @Test + public void roundTripViaBundle_withDefaultValues_yieldsEqualInstance() { + SessionPositionInfo roundTripValue = + SessionPositionInfo.CREATOR.fromBundle(SessionPositionInfo.DEFAULT.toBundle()); + + assertThat(roundTripValue).isEqualTo(SessionPositionInfo.DEFAULT); + } + + @Test + public void toBundle_withDefaultValues_omitsAllData() { + Bundle bundle = + SessionPositionInfo.DEFAULT.toBundle(/* controllerInterfaceVersion= */ Integer.MAX_VALUE); + + assertThat(bundle.isEmpty()).isTrue(); + } + + @Test + public void + toBundle_withDefaultValuesForControllerInterfaceBefore3_includesPositionInfoAndBufferedValues() { + // Controller before version 3 uses invalid default values for indices in PositionInfo and for + // the buffered positions. The Bundle should always include these fields to avoid using the + // invalid defaults. + Bundle bundle = SessionPositionInfo.DEFAULT.toBundle(/* controllerInterfaceVersion= */ 2); + + assertThat(bundle.keySet()) + .containsAtLeast( + SessionPositionInfo.FIELD_BUFFERED_POSITION_MS, + SessionPositionInfo.FIELD_CONTENT_BUFFERED_POSITION_MS, + SessionPositionInfo.FIELD_POSITION_INFO); + } }