From 46a10ec01a0a38ed0335e9fab08064a01e3ea376 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 25 Mar 2020 09:13:04 +0000 Subject: [PATCH] Fix some EventListener logic in ExoPlayerImpl We currently have multiple places in ExoPlayerImpl that assign PlaybackInfo instances and then inform listeners of all current changes. This is not ideal because it causes multiple issues: 1. Some changes may easily be forgotten, e.g. there are clearly some checks missing to see if isPlaying changed (e.g. in seekTo or setMediaSources) 2. Some callbacks didn't check if the value actually changed before sending the callback (e.g. for the timeline change in setMediaSources - if the timeline is still the same, we shouldn't send a onTimelineChanged event). 3. Having multiple callbacks in a single Runnable changes the order of listener invocations slightly: Currently all events for one listener will be send first before moving to the next listener. It should however send a single event to all listeners first before moving to the next event. All these issues can be solved by always using updatePlaybackInfo and never assigning playbackInfo directly in another place. Some tests needed to be updated as well because of issues (2) and (3). Also added a new test to cover issue (1). PiperOrigin-RevId: 302844981 --- .../android/exoplayer2/ExoPlayerImpl.java | 164 ++++++++---------- .../android/exoplayer2/ExoPlayerTest.java | 130 +++++++++++--- 2 files changed, 180 insertions(+), 114 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 745801131c..015c606070 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -432,11 +432,16 @@ import java.util.concurrent.TimeoutException; Timeline oldTimeline = getCurrentTimeline(); pendingOperationAcks++; List holders = addMediaSourceHolders(index, mediaSources); - Timeline timeline = + PlaybackInfo playbackInfo = maskTimelineAndWindowIndex(currentWindowIndex, currentPositionMs, oldTimeline); internalPlayer.addMediaSources(index, holders, shuffleOrder); - notifyListeners( - listener -> listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL, + /* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, + /* seekProcessed= */ false); } @Override @@ -469,11 +474,16 @@ import java.util.concurrent.TimeoutException; pendingOperationAcks++; newFromIndex = Math.min(newFromIndex, mediaSourceHolders.size() - (toIndex - fromIndex)); Playlist.moveMediaSourceHolders(mediaSourceHolders, fromIndex, toIndex, newFromIndex); - Timeline timeline = + PlaybackInfo playbackInfo = maskTimelineAndWindowIndex(currentWindowIndex, currentPositionMs, oldTimeline); internalPlayer.moveMediaSources(fromIndex, toIndex, newFromIndex, shuffleOrder); - notifyListeners( - listener -> listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL, + /* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, + /* seekProcessed= */ false); } @Override @@ -486,13 +496,18 @@ import java.util.concurrent.TimeoutException; @Override public void setShuffleOrder(ShuffleOrder shuffleOrder) { - Timeline timeline = maskTimeline(); + PlaybackInfo playbackInfo = maskTimeline(); maskWithCurrentPosition(); pendingOperationAcks++; this.shuffleOrder = shuffleOrder; internalPlayer.setShuffleOrder(shuffleOrder); - notifyListeners( - listener -> listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL, + /* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, + /* seekProcessed= */ false); } @Override @@ -517,38 +532,26 @@ import java.util.concurrent.TimeoutException; return pauseAtEndOfMediaItems; } - @SuppressWarnings("deprecation") public void setPlayWhenReady( boolean playWhenReady, @PlaybackSuppressionReason int playbackSuppressionReason, @PlayWhenReadyChangeReason int playWhenReadyChangeReason) { - boolean oldIsPlaying = isPlaying(); - boolean playWhenReadyChanged = playbackInfo.playWhenReady != playWhenReady; - boolean suppressionReasonChanged = - playbackInfo.playbackSuppressionReason != playbackSuppressionReason; - if (!playWhenReadyChanged && !suppressionReasonChanged) { + if (playbackInfo.playWhenReady == playWhenReady + && playbackInfo.playbackSuppressionReason == playbackSuppressionReason) { return; } maskWithCurrentPosition(); pendingOperationAcks++; - playbackInfo = playbackInfo.copyWithPlayWhenReady(playWhenReady, playbackSuppressionReason); + PlaybackInfo playbackInfo = + this.playbackInfo.copyWithPlayWhenReady(playWhenReady, playbackSuppressionReason); internalPlayer.setPlayWhenReady(playWhenReady, playbackSuppressionReason); - boolean isPlaying = isPlaying(); - boolean isPlayingChanged = oldIsPlaying != isPlaying; - int playbackState = playbackInfo.playbackState; - notifyListeners( - listener -> { - if (playWhenReadyChanged) { - listener.onPlayerStateChanged(playWhenReady, playbackState); - listener.onPlayWhenReadyChanged(playWhenReady, playWhenReadyChangeReason); - } - if (suppressionReasonChanged) { - listener.onPlaybackSuppressionReasonChanged(playbackSuppressionReason); - } - if (isPlayingChanged) { - listener.onIsPlayingChanged(isPlaying); - } - }); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL, + /* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + playWhenReadyChangeReason, + /* seekProcessed= */ false); } @Override @@ -589,7 +592,6 @@ import java.util.concurrent.TimeoutException; return playbackInfo.isLoading; } - @SuppressWarnings("deprecation") @Override public void seekTo(int windowIndex, long positionMs) { Timeline timeline = playbackInfo.timeline; @@ -597,8 +599,6 @@ import java.util.concurrent.TimeoutException; throw new IllegalSeekPositionException(timeline, windowIndex, positionMs); } hasPendingSeek = true; - boolean playWhenReady = getPlayWhenReady(); - @Player.State int playbackState = getPlaybackState(); pendingOperationAcks++; if (isPlayingAd()) { // TODO: Investigate adding support for seeking during ads. This is complicated to do in @@ -617,18 +617,16 @@ import java.util.concurrent.TimeoutException; maskWindowIndexAndPositionForSeek(timeline, windowIndex, positionMs); @Player.State int newPlaybackState = - playbackState == Player.STATE_IDLE ? Player.STATE_IDLE : Player.STATE_BUFFERING; - boolean playbackStateChanged = playbackState != newPlaybackState; - playbackInfo = playbackInfo.copyWithPlaybackState(newPlaybackState); + getPlaybackState() == Player.STATE_IDLE ? Player.STATE_IDLE : Player.STATE_BUFFERING; + PlaybackInfo playbackInfo = this.playbackInfo.copyWithPlaybackState(newPlaybackState); internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs)); - notifyListeners( - listener -> { - listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK); - if (playbackStateChanged) { - listener.onPlayerStateChanged(playWhenReady, newPlaybackState); - listener.onPlaybackStateChanged(newPlaybackState); - } - }); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ true, + /* positionDiscontinuityReason= */ DISCONTINUITY_REASON_SEEK, + /* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, + /* seekProcessed= */ false); } /** @deprecated Use {@link #setPlaybackSpeed(float)} instead. */ @@ -1011,7 +1009,6 @@ import java.util.concurrent.TimeoutException; seekProcessed)); } - @SuppressWarnings("deprecation") private void setMediaSourcesInternal( List mediaItems, int startWindowIndex, @@ -1019,14 +1016,14 @@ import java.util.concurrent.TimeoutException; boolean resetToDefaultPosition) { int currentWindowIndex = getCurrentWindowIndexInternal(); long currentPositionMs = getCurrentPosition(); - boolean currentPlayWhenReady = getPlayWhenReady(); pendingOperationAcks++; if (!mediaSourceHolders.isEmpty()) { removeMediaSourceHolders( /* fromIndex= */ 0, /* toIndexExclusive= */ mediaSourceHolders.size()); } List holders = addMediaSourceHolders(/* index= */ 0, mediaItems); - Timeline timeline = maskTimeline(); + PlaybackInfo playbackInfo = maskTimeline(); + Timeline timeline = playbackInfo.timeline; if (!timeline.isEmpty() && startWindowIndex >= timeline.getWindowCount()) { throw new IllegalSeekPositionException(timeline, startWindowIndex, startPositionMs); } @@ -1040,9 +1037,9 @@ import java.util.concurrent.TimeoutException; } maskWindowIndexAndPositionForSeek( timeline, startWindowIndex == C.INDEX_UNSET ? 0 : startWindowIndex, startPositionMs); - // mask the playback state + // Mask the playback state. int maskingPlaybackState = playbackInfo.playbackState; - if (startWindowIndex != C.INDEX_UNSET) { + if (startWindowIndex != C.INDEX_UNSET && playbackInfo.playbackState != STATE_IDLE) { // Position reset to startWindowIndex (results in pending initial seek). if (timeline.isEmpty() || startWindowIndex >= timeline.getWindowCount()) { // Setting an empty timeline or invalid seek transitions to ended. @@ -1051,23 +1048,16 @@ import java.util.concurrent.TimeoutException; maskingPlaybackState = STATE_BUFFERING; } } - boolean playbackStateChanged = - playbackInfo.playbackState != STATE_IDLE - && playbackInfo.playbackState != maskingPlaybackState; - int finalMaskingPlaybackState = maskingPlaybackState; - if (playbackStateChanged) { - playbackInfo = playbackInfo.copyWithPlaybackState(finalMaskingPlaybackState); - } + playbackInfo = playbackInfo.copyWithPlaybackState(maskingPlaybackState); internalPlayer.setMediaSources( holders, startWindowIndex, C.msToUs(startPositionMs), shuffleOrder); - notifyListeners( - listener -> { - listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); - if (playbackStateChanged) { - listener.onPlayerStateChanged(currentPlayWhenReady, finalMaskingPlaybackState); - listener.onPlaybackStateChanged(finalMaskingPlaybackState); - } - }); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ Player.DISCONTINUITY_REASON_INTERNAL, + /* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, + /* seekProcessed= */ false); } private List addMediaSourceHolders( @@ -1085,18 +1075,16 @@ import java.util.concurrent.TimeoutException; return holders; } - @SuppressWarnings("deprecation") private void removeMediaItemsInternal(int fromIndex, int toIndex) { Assertions.checkArgument( fromIndex >= 0 && toIndex >= fromIndex && toIndex <= mediaSourceHolders.size()); int currentWindowIndex = getCurrentWindowIndex(); long currentPositionMs = getCurrentPosition(); - boolean currentPlayWhenReady = getPlayWhenReady(); Timeline oldTimeline = getCurrentTimeline(); int currentMediaSourceCount = mediaSourceHolders.size(); pendingOperationAcks++; removeMediaSourceHolders(fromIndex, /* toIndexExclusive= */ toIndex); - Timeline timeline = + PlaybackInfo playbackInfo = maskTimelineAndWindowIndex(currentWindowIndex, currentPositionMs, oldTimeline); // Player transitions to STATE_ENDED if the current index is part of the removed tail. final boolean transitionsToEnded = @@ -1104,19 +1092,18 @@ import java.util.concurrent.TimeoutException; && playbackInfo.playbackState != STATE_ENDED && fromIndex < toIndex && toIndex == currentMediaSourceCount - && currentWindowIndex >= timeline.getWindowCount(); + && currentWindowIndex >= playbackInfo.timeline.getWindowCount(); if (transitionsToEnded) { playbackInfo = playbackInfo.copyWithPlaybackState(STATE_ENDED); } internalPlayer.removeMediaSources(fromIndex, toIndex, shuffleOrder); - notifyListeners( - listener -> { - listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); - if (transitionsToEnded) { - listener.onPlayerStateChanged(currentPlayWhenReady, STATE_ENDED); - listener.onPlaybackStateChanged(STATE_ENDED); - } - }); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ Player.DISCONTINUITY_REASON_INTERNAL, + /* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, + /* seekProcessed= */ false); } private List removeMediaSourceHolders( @@ -1129,18 +1116,17 @@ import java.util.concurrent.TimeoutException; return removed; } - private Timeline maskTimeline() { - playbackInfo = - playbackInfo.copyWithTimeline( - mediaSourceHolders.isEmpty() - ? Timeline.EMPTY - : new Playlist.PlaylistTimeline(mediaSourceHolders, shuffleOrder)); - return playbackInfo.timeline; + private PlaybackInfo maskTimeline() { + return playbackInfo.copyWithTimeline( + mediaSourceHolders.isEmpty() + ? Timeline.EMPTY + : new Playlist.PlaylistTimeline(mediaSourceHolders, shuffleOrder)); } - private Timeline maskTimelineAndWindowIndex( + private PlaybackInfo maskTimelineAndWindowIndex( int currentWindowIndex, long currentPositionMs, Timeline oldTimeline) { - Timeline maskingTimeline = maskTimeline(); + PlaybackInfo playbackInfo = maskTimeline(); + Timeline maskingTimeline = playbackInfo.timeline; if (oldTimeline.isEmpty()) { // The index is the default index or was set by a seek in the empty old timeline. maskingWindowIndex = currentWindowIndex; @@ -1148,7 +1134,7 @@ import java.util.concurrent.TimeoutException; // The seek is not valid in the new timeline. maskWithDefaultPosition(maskingTimeline); } - return maskingTimeline; + return playbackInfo; } @Nullable Pair periodPosition = @@ -1186,7 +1172,7 @@ import java.util.concurrent.TimeoutException; maskWithDefaultPosition(maskingTimeline); } } - return maskingTimeline; + return playbackInfo; } private void maskWindowIndexAndPositionForSeek( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index cf92c77840..035cf542b1 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -3391,7 +3391,6 @@ public final class ExoPlayerTest { .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); exoPlayerTestRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); assertArrayEquals(new long[] {2}, windowCounts); assertArrayEquals(new int[] {seekToWindowIndex}, currentWindowIndices); @@ -4157,7 +4156,7 @@ public final class ExoPlayerTest { int seekToWindowIndex = 1; ActionSchedule actionSchedule = new ActionSchedule.Builder(TAG) - .waitForTimelineChanged() + .waitForPlaybackState(Player.STATE_ENDED) .executeRunnable( new PlayerRunnable() { @Override @@ -4168,12 +4167,11 @@ public final class ExoPlayerTest { } }) .executeRunnable( - () -> { - concatenatingMediaSource.addMediaSource( - new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT)); - concatenatingMediaSource.addMediaSource( - new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT)); - }) + () -> + concatenatingMediaSource.addMediaSources( + Arrays.asList( + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT)))) .waitForTimelineChanged() .executeRunnable( new PlayerRunnable() { @@ -4203,7 +4201,7 @@ public final class ExoPlayerTest { ConcatenatingMediaSource concatenatingMediaSource = new ConcatenatingMediaSource(/* isAtomic= */ false); ActionSchedule actionSchedule = - new ActionSchedule.Builder(TAG).waitForTimelineChanged().build(); + new ActionSchedule.Builder(TAG).waitForPlaybackState(Player.STATE_ENDED).build(); ExoPlayerTestRunner exoPlayerTestRunner = new Builder() .setMediaSources(concatenatingMediaSource) @@ -4229,7 +4227,7 @@ public final class ExoPlayerTest { int seekToWindowIndex = 1; ActionSchedule actionSchedule = new ActionSchedule.Builder(TAG) - .waitForTimelineChanged() + .waitForPlaybackState(Player.STATE_ENDED) .executeRunnable( new PlayerRunnable() { @Override @@ -4240,12 +4238,11 @@ public final class ExoPlayerTest { } }) .executeRunnable( - () -> { - concatenatingMediaSource.addMediaSource( - new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT)); - concatenatingMediaSource.addMediaSource( - new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT)); - }) + () -> + concatenatingMediaSource.addMediaSources( + Arrays.asList( + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT)))) .waitForTimelineChanged() .executeRunnable( new PlayerRunnable() { @@ -4727,7 +4724,6 @@ public final class ExoPlayerTest { new int[] {Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_IDLE}, maskingPlaybackStates); exoPlayerTestRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); @@ -4768,7 +4764,6 @@ public final class ExoPlayerTest { Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED); assertArrayEquals(new int[] {Player.STATE_IDLE}, maskingPlaybackStates); exoPlayerTestRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); } @@ -4841,7 +4836,6 @@ public final class ExoPlayerTest { Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED); assertArrayEquals(new int[] {Player.STATE_IDLE}, maskingPlaybackStates); exoPlayerTestRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); } @@ -4924,9 +4918,6 @@ public final class ExoPlayerTest { }, maskingPlaybackStates); exoPlayerTestRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, @@ -4969,7 +4960,6 @@ public final class ExoPlayerTest { exoPlayerTestRunner.assertPlaybackStatesEqual(Player.STATE_ENDED); assertArrayEquals(new int[] {Player.STATE_ENDED}, maskingPlaybackStates); exoPlayerTestRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); } @@ -5050,7 +5040,6 @@ public final class ExoPlayerTest { exoPlayerTestRunner.assertTimelineChangeReasonsEqual( Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); } @@ -5213,7 +5202,6 @@ public final class ExoPlayerTest { Player.STATE_ENDED); assertArrayEquals(new int[] {Player.STATE_ENDED}, maskingPlaybackStates); exoPlayerTestRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, @@ -6161,6 +6149,7 @@ public final class ExoPlayerTest { public void loading_withLargeAllocationCausingOom_playsRemainingMediaAndThenThrows() { Loader.Loadable loadable = new Loader.Loadable() { + @SuppressWarnings("UnusedVariable") @Override public void load() throws IOException { @SuppressWarnings("unused") // This test needs the allocation to cause an OOM. @@ -6229,6 +6218,97 @@ public final class ExoPlayerTest { assertThat(renderer.sampleBufferReadCount).isEqualTo(3); } + @Test + public void seekTo_whileReady_callsOnIsPlayingChanged() throws Exception { + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .waitForPlaybackState(Player.STATE_READY) + .seek(/* positionMs= */ 0) + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + List onIsPlayingChanges = new ArrayList<>(); + Player.EventListener eventListener = + new Player.EventListener() { + @Override + public void onIsPlayingChanged(boolean isPlaying) { + onIsPlayingChanges.add(isPlaying); + } + }; + new ExoPlayerTestRunner.Builder() + .setEventListener(eventListener) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + assertThat(onIsPlayingChanges).containsExactly(true, false, true, false).inOrder(); + } + + @Test + public void multipleListenersAndMultipleCallbacks_callbacksAreOrderedByType() throws Exception { + String playWhenReadyChange1 = "playWhenReadyChange1"; + String playWhenReadyChange2 = "playWhenReadyChange2"; + String isPlayingChange1 = "isPlayingChange1"; + String isPlayingChange2 = "isPlayingChange2"; + ArrayList events = new ArrayList<>(); + Player.EventListener eventListener1 = + new Player.EventListener() { + @Override + public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) { + events.add(playWhenReadyChange1); + } + + @Override + public void onIsPlayingChanged(boolean isPlaying) { + events.add(isPlayingChange1); + } + }; + Player.EventListener eventListener2 = + new Player.EventListener() { + @Override + public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) { + events.add(playWhenReadyChange2); + } + + @Override + public void onIsPlayingChanged(boolean isPlaying) { + events.add(isPlayingChange2); + } + }; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .pause() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.addListener(eventListener1); + player.addListener(eventListener2); + } + }) + .waitForPlaybackState(Player.STATE_READY) + .play() + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + new ExoPlayerTestRunner.Builder() + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + assertThat(events) + .containsExactly( + playWhenReadyChange1, + playWhenReadyChange2, + isPlayingChange1, + isPlayingChange2, + isPlayingChange1, + isPlayingChange2) + .inOrder(); + } + // Internal methods. private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {