diff --git a/libraries/common/src/main/java/androidx/media3/common/AdPlaybackState.java b/libraries/common/src/main/java/androidx/media3/common/AdPlaybackState.java index 0fb4297afe..cc6557646b 100644 --- a/libraries/common/src/main/java/androidx/media3/common/AdPlaybackState.java +++ b/libraries/common/src/main/java/androidx/media3/common/AdPlaybackState.java @@ -299,6 +299,28 @@ public final class AdPlaybackState implements Bundleable { timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted); } + /** + * Returns an instance with all ads in final states (played, skipped, error) reset to either + * available or unavailable, which allows to play them again. + */ + @CheckResult + public AdGroup withAllAdsReset() { + if (count == C.LENGTH_UNSET) { + return this; + } + int count = this.states.length; + @AdState int[] states = Arrays.copyOf(this.states, count); + for (int i = 0; i < count; i++) { + if (states[i] == AD_STATE_PLAYED + || states[i] == AD_STATE_SKIPPED + || states[i] == AD_STATE_ERROR) { + states[i] = uris[i] == null ? AD_STATE_UNAVAILABLE : AD_STATE_AVAILABLE; + } + } + return new AdGroup( + timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted); + } + @CheckResult private static @AdState int[] copyStatesWithSpaceForAdCount(@AdState int[] states, int count) { int oldStateCount = states.length; @@ -782,6 +804,19 @@ public final class AdPlaybackState implements Bundleable { adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount); } + /** + * Returns an instance with all ads in the specified ad group reset from final states (played, + * skipped, error) to either available or unavailable, which allows to play them again. + */ + @CheckResult + public AdPlaybackState withResetAdGroup(@IntRange(from = 0) int adGroupIndex) { + int adjustedIndex = adGroupIndex - removedAdGroupCount; + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); + adGroups[adjustedIndex] = adGroups[adjustedIndex].withAllAdsReset(); + return new AdPlaybackState( + adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount); + } + @Override public boolean equals(@Nullable Object o) { if (this == o) { diff --git a/libraries/common/src/test/java/androidx/media3/common/AdPlaybackStateTest.java b/libraries/common/src/test/java/androidx/media3/common/AdPlaybackStateTest.java index b7be74a996..bf60d525fb 100644 --- a/libraries/common/src/test/java/androidx/media3/common/AdPlaybackStateTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/AdPlaybackStateTest.java @@ -16,7 +16,10 @@ package androidx.media3.common; import static androidx.media3.common.AdPlaybackState.AD_STATE_AVAILABLE; +import static androidx.media3.common.AdPlaybackState.AD_STATE_ERROR; import static androidx.media3.common.AdPlaybackState.AD_STATE_PLAYED; +import static androidx.media3.common.AdPlaybackState.AD_STATE_SKIPPED; +import static androidx.media3.common.AdPlaybackState.AD_STATE_UNAVAILABLE; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; @@ -253,6 +256,60 @@ public class AdPlaybackStateTest { assertThat(state.getAdGroup(1).count).isEqualTo(0); } + @Test + public void withResetAdGroup_beforeSetAdCount_doesNothing() { + AdPlaybackState state = new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US); + + state = state.withResetAdGroup(/* adGroupIndex= */ 1); + + assertThat(state.getAdGroup(1).count).isEqualTo(C.LENGTH_UNSET); + } + + @Test + public void withResetAdGroup_resetsAdsInFinalStates() { + AdPlaybackState state = new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US); + state = state.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 5); + state = + state.withAdDurationsUs( + /* adGroupIndex= */ 1, /* adDurationsUs...= */ 1_000L, 2_000L, 3_000L, 4_000L, 5_000L); + state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, Uri.EMPTY); + state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, Uri.EMPTY); + state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 3, Uri.EMPTY); + state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 4, Uri.EMPTY); + state = state.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2); + state = state.withSkippedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 3); + state = state.withAdLoadError(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 4); + // Verify setup. + assertThat(state.getAdGroup(/* adGroupIndex= */ 1).states) + .asList() + .containsExactly( + AD_STATE_UNAVAILABLE, + AD_STATE_AVAILABLE, + AD_STATE_PLAYED, + AD_STATE_SKIPPED, + AD_STATE_ERROR) + .inOrder(); + + state = state.withResetAdGroup(/* adGroupIndex= */ 1); + + assertThat(state.getAdGroup(/* adGroupIndex= */ 1).states) + .asList() + .containsExactly( + AD_STATE_UNAVAILABLE, + AD_STATE_AVAILABLE, + AD_STATE_AVAILABLE, + AD_STATE_AVAILABLE, + AD_STATE_AVAILABLE) + .inOrder(); + assertThat(state.getAdGroup(/* adGroupIndex= */ 1).uris) + .asList() + .containsExactly(null, Uri.EMPTY, Uri.EMPTY, Uri.EMPTY, Uri.EMPTY) + .inOrder(); + assertThat(state.getAdGroup(/* adGroupIndex= */ 1).durationsUs) + .asList() + .containsExactly(1_000L, 2_000L, 3_000L, 4_000L, 5_000L); + } + @Test public void roundTripViaBundle_yieldsEqualFieldsExceptAdsId() { AdPlaybackState originalState =