diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/Clock.java b/library/common/src/main/java/com/google/android/exoplayer2/util/Clock.java index f6b98a1c66..8ecb2ab8ec 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/Clock.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/Clock.java @@ -50,4 +50,12 @@ public interface Clock { * @see Handler#Handler(Looper, Handler.Callback) */ HandlerWrapper createHandler(Looper looper, @Nullable Handler.Callback callback); + + /** + * Notifies the clock that the current thread is about to be blocked and won't return until a + * condition on another thread becomes true. + * + *

Should be a no-op for all non-test cases. + */ + void onThreadBlocked(); } diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/SystemClock.java b/library/common/src/main/java/com/google/android/exoplayer2/util/SystemClock.java index e315d8bf25..c3b31aa5c9 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/SystemClock.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/SystemClock.java @@ -47,4 +47,9 @@ public class SystemClock implements Clock { public HandlerWrapper createHandler(Looper looper, @Nullable Callback callback) { return new SystemHandlerWrapper(new Handler(looper, callback)); } + + @Override + public void onThreadBlocked() { + // Do nothing. + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index a97a38e8ef..5a2c783a6f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -624,6 +624,7 @@ import java.util.concurrent.atomic.AtomicBoolean; boolean wasInterrupted = false; while (!condition.get() && remainingMs > 0) { try { + clock.onThreadBlocked(); wait(remainingMs); } catch (InterruptedException e) { wasInterrupted = true; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java index 36f562f7cb..4191480700 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java @@ -341,6 +341,7 @@ public final class PlayerMessage { long deadlineMs = clock.elapsedRealtime() + timeoutMs; long remainingMs = timeoutMs; while (!isProcessed && remainingMs > 0) { + clock.onThreadBlocked(); wait(remainingMs); remainingMs = deadlineMs - clock.elapsedRealtime(); } 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 60e96308e3..d163e704a5 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 @@ -130,7 +130,6 @@ import org.mockito.ArgumentMatcher; import org.mockito.InOrder; import org.mockito.Mockito; import org.robolectric.shadows.ShadowAudioManager; -import org.robolectric.shadows.ShadowLooper; /** Unit test for {@link ExoPlayer}. */ @RunWith(AndroidJUnit4.class) @@ -1003,11 +1002,15 @@ public final class ExoPlayerTest { .waitForPlaybackState(Player.STATE_BUFFERING) // Block until createPeriod has been called on the fake media source. .executeRunnable( - () -> { - try { - createPeriodCalledCountDownLatch.await(); - } catch (InterruptedException e) { - throw new IllegalStateException(e); + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + try { + player.getClock().onThreadBlocked(); + createPeriodCalledCountDownLatch.await(); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } } }) // Set playback speed (while the fake media period is not yet prepared). @@ -1023,7 +1026,6 @@ public final class ExoPlayerTest { .blockUntilEnded(TIMEOUT_MS); } - @Ignore // Temporarily disabled because the AutoAdvancingFakeClock picks up the wrong thread. @Test public void seekBeforePreparationCompletes_seeksToCorrectPosition() throws Exception { CountDownLatch createPeriodCalledCountDownLatch = new CountDownLatch(1); @@ -2043,7 +2045,6 @@ public final class ExoPlayerTest { assertThat(target80.positionMs).isAtLeast(target50.positionMs); } - @Ignore // Temporarily disabled because the AutoAdvancingFakeClock picks up the wrong thread. @Test public void sendMessagesFromStartPositionOnlyOnce() throws Exception { AtomicInteger counter = new AtomicInteger(); @@ -2820,6 +2821,7 @@ public final class ExoPlayerTest { // seek in the timeline which still has two windows in EPI, but when the seek // arrives in EPII the actual timeline has one window only. Hence it tries to // find the subsequent period of the removed period and finds it. + player.getClock().onThreadBlocked(); sourceReleasedCountDownLatch.await(); } catch (InterruptedException e) { throw new IllegalStateException(e); @@ -2961,7 +2963,6 @@ public final class ExoPlayerTest { assertThat(sequence).containsExactly(0, 1, 2).inOrder(); } - @Ignore // Temporarily disabled because the AutoAdvancingFakeClock picks up the wrong thread. @Test public void recursiveTimelineChangeInStopAreReportedInCorrectOrder() throws Exception { Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 2); @@ -4243,7 +4244,7 @@ public final class ExoPlayerTest { createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 4000)); assertThat(windowIndex[0]).isEqualTo(0); - assertThat(positionMs[0]).isGreaterThan(8000); + assertThat(positionMs[0]).isEqualTo(8000); assertThat(bufferedPositions[0]).isEqualTo(10_000); assertThat(totalBufferedDuration[0]).isEqualTo(10_000 - positionMs[0]); @@ -4470,7 +4471,7 @@ public final class ExoPlayerTest { assertThat(windowIndex[2]).isEqualTo(0); assertThat(isPlayingAd[2]).isFalse(); - assertThat(positionMs[2]).isGreaterThan(8000); + assertThat(positionMs[2]).isEqualTo(8000); assertThat(bufferedPositionMs[2]).isEqualTo(contentDurationMs); assertThat(totalBufferedDurationMs[2]).isAtLeast(contentDurationMs - positionMs[2]); } @@ -4592,68 +4593,32 @@ public final class ExoPlayerTest { runUntilPlaybackState(player, Player.STATE_ENDED); } - @Ignore // Temporarily disabled because the AutoAdvancingFakeClock picks up the wrong thread. @Test public void becomingNoisyIgnoredIfBecomingNoisyHandlingIsDisabled() throws Exception { - CountDownLatch becomingNoisyHandlingDisabled = new CountDownLatch(1); - CountDownLatch becomingNoisyDelivered = new CountDownLatch(1); - PlayerStateGrabber playerStateGrabber = new PlayerStateGrabber(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder(TAG) - .executeRunnable( - new PlayerRunnable() { - @Override - public void run(SimpleExoPlayer player) { - player.setHandleAudioBecomingNoisy(false); - becomingNoisyHandlingDisabled.countDown(); + SimpleExoPlayer player = new TestExoPlayerBuilder(context).build(); + player.play(); - // Wait for the broadcast to be delivered from the main thread. - try { - becomingNoisyDelivered.await(); - } catch (InterruptedException e) { - throw new IllegalStateException(e); - } - } - }) - .delay(1) // Handle pending messages on the playback thread. - .executeRunnable(playerStateGrabber) - .build(); - - ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder(context).setActionSchedule(actionSchedule).build().start(); - becomingNoisyHandlingDisabled.await(); + player.setHandleAudioBecomingNoisy(false); deliverBroadcast(new Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY)); - becomingNoisyDelivered.countDown(); + TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player); + boolean playWhenReadyAfterBroadcast = player.getPlayWhenReady(); + player.release(); - testRunner.blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); - assertThat(playerStateGrabber.playWhenReady).isTrue(); + assertThat(playWhenReadyAfterBroadcast).isTrue(); } @Test public void pausesWhenBecomingNoisyIfBecomingNoisyHandlingIsEnabled() throws Exception { - CountDownLatch becomingNoisyHandlingEnabled = new CountDownLatch(1); - ActionSchedule actionSchedule = - new ActionSchedule.Builder(TAG) - .executeRunnable( - new PlayerRunnable() { - @Override - public void run(SimpleExoPlayer player) { - player.setHandleAudioBecomingNoisy(true); - becomingNoisyHandlingEnabled.countDown(); - } - }) - .waitForPlayWhenReady(false) // Becoming noisy should set playWhenReady = false - .play() - .build(); + SimpleExoPlayer player = new TestExoPlayerBuilder(context).build(); + player.play(); - ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder(context).setActionSchedule(actionSchedule).build().start(); - becomingNoisyHandlingEnabled.await(); + player.setHandleAudioBecomingNoisy(true); deliverBroadcast(new Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY)); + TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player); + boolean playWhenReadyAfterBroadcast = player.getPlayWhenReady(); + player.release(); - // If the player fails to handle becoming noisy, blockUntilActionScheduleFinished will time out - // and throw, causing the test to fail. - testRunner.blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); + assertThat(playWhenReadyAfterBroadcast).isFalse(); } @Test @@ -7003,7 +6968,7 @@ public final class ExoPlayerTest { }, // buffers after set items with seek maskingPlaybackStates); assertArrayEquals(new int[] {2, 0, 0, 1, 1, 0, 0, 0, 0}, currentWindowIndices); - assertThat(currentPositions[0]).isGreaterThan(0); + assertThat(currentPositions[0]).isEqualTo(0); assertThat(currentPositions[1]).isEqualTo(0); assertThat(currentPositions[2]).isEqualTo(0); assertThat(bufferedPositions[0]).isGreaterThan(0); @@ -8891,7 +8856,7 @@ public final class ExoPlayerTest { player.setMediaSource(new FakeMediaSource(new FakeTimeline(), formatWithStaticMetadata)); player.seekTo(2_000); player.setPlaybackParameters(new PlaybackParameters(/* speed= */ 2.0f)); - ShadowLooper.runMainLooperToNextTask(); + TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player); verify(listener).onTimelineChanged(any(), anyInt()); verify(listener).onMediaItemTransition(any(), anyInt()); @@ -8914,7 +8879,7 @@ public final class ExoPlayerTest { } }); player.setRepeatMode(Player.REPEAT_MODE_ONE); - ShadowLooper.runMainLooperToNextTask(); + TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player); verify(listener).onRepeatModeChanged(anyInt()); verify(listener).onShuffleModeEnabledChanged(anyBoolean()); @@ -8930,7 +8895,7 @@ public final class ExoPlayerTest { player.play(); player.setMediaItem(MediaItem.fromUri("http://this-will-throw-an-exception.mp4")); TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_IDLE); - ShadowLooper.runMainLooperToNextTask(); + TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player); player.release(); // Verify that all callbacks have been called at least once. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/MetadataRetrieverTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MetadataRetrieverTest.java index 53f6c24f10..154ec0df1b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/MetadataRetrieverTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/MetadataRetrieverTest.java @@ -40,6 +40,7 @@ import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.shadows.ShadowLooper; /** Tests for {@link MetadataRetriever}. */ @RunWith(AndroidJUnit4.class) @@ -63,6 +64,7 @@ public class MetadataRetrieverTest { ListenableFuture trackGroupsFuture = retrieveMetadata(context, mediaItem, clock); + ShadowLooper.idleMainLooper(); TrackGroupArray trackGroups = trackGroupsFuture.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS); assertThat(trackGroups.length).isEqualTo(2); @@ -85,6 +87,7 @@ public class MetadataRetrieverTest { retrieveMetadata(context, mediaItem1, clock); ListenableFuture trackGroupsFuture2 = retrieveMetadata(context, mediaItem2, clock); + ShadowLooper.idleMainLooper(); TrackGroupArray trackGroups1 = trackGroupsFuture1.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS); TrackGroupArray trackGroups2 = trackGroupsFuture2.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS); @@ -118,6 +121,7 @@ public class MetadataRetrieverTest { ListenableFuture trackGroupsFuture = retrieveMetadata(context, mediaItem, clock); + ShadowLooper.idleMainLooper(); TrackGroupArray trackGroups = trackGroupsFuture.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS); assertThat(trackGroups.length).isEqualTo(1); @@ -134,6 +138,7 @@ public class MetadataRetrieverTest { ListenableFuture trackGroupsFuture = retrieveMetadata(context, mediaItem, clock); + ShadowLooper.idleMainLooper(); TrackGroupArray trackGroups = trackGroupsFuture.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS); assertThat(trackGroups.length).isEqualTo(1); @@ -164,6 +169,7 @@ public class MetadataRetrieverTest { ListenableFuture trackGroupsFuture = retrieveMetadata(context, mediaItem, clock); + ShadowLooper.idleMainLooper(); TrackGroupArray trackGroups = trackGroupsFuture.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS); assertThat(trackGroups.length).isEqualTo(2); // Video and audio @@ -185,6 +191,7 @@ public class MetadataRetrieverTest { ListenableFuture trackGroupsFuture = retrieveMetadata(context, mediaItem, clock); + ShadowLooper.idleMainLooper(); assertThrows( ExecutionException.class, () -> trackGroupsFuture.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS)); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java index ad807c4079..bc7d149007 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java @@ -215,7 +215,7 @@ public final class AnalyticsCollectorTest { period0 /* ENDED */) .inOrder(); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* SOURCE_UPDATE */) + .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* SOURCE_UPDATE */) .inOrder(); assertThat(listener.getEvents(EVENT_IS_LOADING_CHANGED)) .containsExactly(period0 /* started */, period0 /* stopped */) @@ -656,9 +656,9 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) .containsExactly( WINDOW_0 /* PLAYLIST_CHANGE */, - WINDOW_0 /* SOURCE_UPDATE */, + period0Seq0 /* SOURCE_UPDATE */, WINDOW_0 /* PLAYLIST_CHANGE */, - WINDOW_0 /* SOURCE_UPDATE */); + period0Seq1 /* SOURCE_UPDATE */); assertThat(listener.getEvents(EVENT_IS_LOADING_CHANGED)) .containsExactly(period0Seq0, period0Seq0, period0Seq1, period0Seq1) .inOrder(); @@ -748,7 +748,7 @@ public final class AnalyticsCollectorTest { period0Seq0 /* ENDED */) .inOrder(); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly(WINDOW_0 /* prepared */, WINDOW_0 /* prepared */); + .containsExactly(WINDOW_0 /* prepared */, period0Seq0 /* prepared */); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period0Seq0); assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0Seq0); assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period0Seq0); @@ -929,7 +929,7 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) .containsExactly( WINDOW_0 /* PLAYLIST_CHANGED */, - WINDOW_0 /* SOURCE_UPDATE (first item) */, + period0Seq0 /* SOURCE_UPDATE (first item) */, period0Seq0 /* PLAYLIST_CHANGED (add) */, period0Seq0 /* SOURCE_UPDATE (second item) */, period0Seq1 /* PLAYLIST_CHANGED (remove) */) @@ -949,7 +949,7 @@ public final class AnalyticsCollectorTest { .containsExactly(period0Seq0, period1Seq1) .inOrder(); assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) - .containsExactly(period0Seq0, period1Seq1, period0Seq1) + .containsExactly(period0Seq0, period0Seq1) .inOrder(); assertThat(listener.getEvents(EVENT_DECODER_INIT)) .containsExactly(period0Seq0, period1Seq1) @@ -957,10 +957,9 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) .containsExactly(period0Seq0, period1Seq1) .inOrder(); - assertThat(listener.getEvents(EVENT_DECODER_DISABLED)) - .containsExactly(period0Seq0, period0Seq0); + assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(period0Seq0); assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)) - .containsExactly(period0Seq0, period1Seq1, period0Seq1) + .containsExactly(period0Seq0, period0Seq1) .inOrder(); assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INITIALIZED)) .containsExactly(period0Seq0, period1Seq1) @@ -968,13 +967,13 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_VIDEO_INPUT_FORMAT_CHANGED)) .containsExactly(period0Seq0, period1Seq1) .inOrder(); - assertThat(listener.getEvents(EVENT_VIDEO_DISABLED)).containsExactly(period0Seq0, period0Seq0); + assertThat(listener.getEvents(EVENT_VIDEO_DISABLED)).containsExactly(period0Seq0); assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(period0Seq1); assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)) - .containsExactly(period0Seq0, period0Seq1) + .containsExactly(period0Seq0, period1Seq1, period0Seq1) .inOrder(); assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)) - .containsExactly(period0Seq0, period0Seq1); + .containsExactly(period0Seq0, period1Seq1, period0Seq1); assertThat(listener.getEvents(EVENT_VIDEO_FRAME_PROCESSING_OFFSET)) .containsExactly(period0Seq1); listener.assertNoMoreEvents(); @@ -1132,7 +1131,7 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) .containsExactly( WINDOW_0 /* PLAYLIST_CHANGED */, - WINDOW_0 /* SOURCE_UPDATE (initial) */, + prerollAd /* SOURCE_UPDATE (initial) */, contentAfterPreroll /* SOURCE_UPDATE (played preroll) */, contentAfterMidroll /* SOURCE_UPDATE (played midroll) */, contentAfterPostroll /* SOURCE_UPDATE (played postroll) */) @@ -1327,7 +1326,7 @@ public final class AnalyticsCollectorTest { contentAfterMidroll /* ENDED */) .inOrder(); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* SOURCE_UPDATE */); + .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, contentBeforeMidroll /* SOURCE_UPDATE */); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)) .containsExactly( contentAfterMidroll /* seek */, diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java index c736444a43..bd5dfb97a5 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java @@ -15,13 +15,13 @@ */ package com.google.android.exoplayer2.analytics; +import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.robolectric.shadows.ShadowLooper.runMainLooperToNextTask; import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; @@ -42,6 +42,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.robolectric.shadows.ShadowLooper; /** Unit test for {@link PlaybackStatsListener}. */ @RunWith(AndroidJUnit4.class) @@ -60,41 +61,41 @@ public final class PlaybackStatsListenerTest { } @Test - public void events_duringInitialIdleState_dontCreateNewPlaybackStats() { + public void events_duringInitialIdleState_dontCreateNewPlaybackStats() throws Exception { PlaybackStatsListener playbackStatsListener = new PlaybackStatsListener(/* keepHistory= */ true, /* callback= */ null); player.addAnalyticsListener(playbackStatsListener); player.seekTo(/* positionMs= */ 1234); - runMainLooperToNextTask(); + runUntilPendingCommandsAreFullyHandled(player); player.setPlaybackParameters(new PlaybackParameters(/* speed= */ 2f)); - runMainLooperToNextTask(); + runUntilPendingCommandsAreFullyHandled(player); player.play(); - runMainLooperToNextTask(); + runUntilPendingCommandsAreFullyHandled(player); assertThat(playbackStatsListener.getPlaybackStats()).isNull(); } @Test - public void stateChangeEvent_toNonIdle_createsInitialPlaybackStats() { + public void stateChangeEvent_toNonIdle_createsInitialPlaybackStats() throws Exception { PlaybackStatsListener playbackStatsListener = new PlaybackStatsListener(/* keepHistory= */ true, /* callback= */ null); player.addAnalyticsListener(playbackStatsListener); player.prepare(); - runMainLooperToNextTask(); + runUntilPendingCommandsAreFullyHandled(player); assertThat(playbackStatsListener.getPlaybackStats()).isNotNull(); } @Test - public void timelineChangeEvent_toNonEmpty_createsInitialPlaybackStats() { + public void timelineChangeEvent_toNonEmpty_createsInitialPlaybackStats() throws Exception { PlaybackStatsListener playbackStatsListener = new PlaybackStatsListener(/* keepHistory= */ true, /* callback= */ null); player.addAnalyticsListener(playbackStatsListener); player.setMediaItem(MediaItem.fromUri("http://test.org")); - runMainLooperToNextTask(); + runUntilPendingCommandsAreFullyHandled(player); assertThat(playbackStatsListener.getPlaybackStats()).isNotNull(); } @@ -109,7 +110,7 @@ public final class PlaybackStatsListenerTest { player.prepare(); player.play(); TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED); - runMainLooperToNextTask(); + runUntilPendingCommandsAreFullyHandled(player); @Nullable PlaybackStats playbackStats = playbackStatsListener.getPlaybackStats(); assertThat(playbackStats).isNotNull(); @@ -126,7 +127,7 @@ public final class PlaybackStatsListenerTest { player.prepare(); player.play(); TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED); - runMainLooperToNextTask(); + runUntilPendingCommandsAreFullyHandled(player); @Nullable PlaybackStats playbackStats = playbackStatsListener.getPlaybackStats(); assertThat(playbackStats).isNotNull(); @@ -134,7 +135,7 @@ public final class PlaybackStatsListenerTest { } @Test - public void finishedSession_callsCallback() { + public void finishedSession_callsCallback() throws Exception { PlaybackStatsListener.Callback callback = mock(PlaybackStatsListener.Callback.class); PlaybackStatsListener playbackStatsListener = new PlaybackStatsListener(/* keepHistory= */ true, callback); @@ -143,10 +144,10 @@ public final class PlaybackStatsListenerTest { // Create session with some events and finish it by removing it from the playlist. player.setMediaSource(new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1))); player.prepare(); - runMainLooperToNextTask(); + runUntilPendingCommandsAreFullyHandled(player); verify(callback, never()).onPlaybackStatsReady(any(), any()); player.clearMediaItems(); - runMainLooperToNextTask(); + runUntilPendingCommandsAreFullyHandled(player); verify(callback).onPlaybackStatsReady(any(), any()); } @@ -166,9 +167,9 @@ public final class PlaybackStatsListenerTest { // the first one isn't finished yet. TestPlayerRunHelper.playUntilPosition( player, /* windowIndex= */ 0, /* positionMs= */ player.getDuration()); - runMainLooperToNextTask(); + runUntilPendingCommandsAreFullyHandled(player); player.release(); - runMainLooperToNextTask(); + ShadowLooper.idleMainLooper(); ArgumentCaptor eventTimeCaptor = ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); diff --git a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java index 12f799fc53..d3ef423217 100644 --- a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java +++ b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java @@ -143,9 +143,6 @@ public final class TransformerTest { TransformerTestRunner.runUntilCompleted(transformer); Files.delete(Paths.get(outputPath)); - // Transformer.startTransformation() will create a new SimpleExoPlayer instance. Reset the - // clock's handler so that the clock advances with the new SimpleExoPlayer instance. - clock.resetHandler(); // Transform second media item. transformer.startTransformation(mediaItem, outputPath); TransformerTestRunner.runUntilCompleted(transformer); @@ -236,9 +233,7 @@ public final class TransformerTest { transformer.startTransformation(mediaItem, outputPath); transformer.cancel(); Files.delete(Paths.get(outputPath)); - // Transformer.startTransformation() will create a new SimpleExoPlayer instance. Reset the - // clock's handler so that the clock advances with the new SimpleExoPlayer instance. - clock.resetHandler(); + // This would throw if the previous transformation had not been cancelled. transformer.startTransformation(mediaItem, outputPath); TransformerTestRunner.runUntilCompleted(transformer); diff --git a/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/TestPlayerRunHelper.java b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/TestPlayerRunHelper.java index fe67af3d93..b813a93d2a 100644 --- a/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/TestPlayerRunHelper.java +++ b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/TestPlayerRunHelper.java @@ -313,6 +313,7 @@ public class TestPlayerRunHelper { blockPlaybackThreadCondition.open(); }); try { + player.getClock().onThreadBlocked(); blockPlaybackThreadCondition.block(); } catch (InterruptedException e) { // Ignore. diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index fb0ee74bae..003c3eb3ba 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -701,6 +701,7 @@ public abstract class Action { blockPlaybackThreadCondition.open(); }); try { + player.getClock().onThreadBlocked(); blockPlaybackThreadCondition.block(); } catch (InterruptedException e) { // Ignore. diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/AutoAdvancingFakeClock.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/AutoAdvancingFakeClock.java index d9a789ea18..86b9bb39f3 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/AutoAdvancingFakeClock.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/AutoAdvancingFakeClock.java @@ -15,23 +15,12 @@ */ package com.google.android.exoplayer2.testutil; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.util.HandlerWrapper; - /** * {@link FakeClock} extension which automatically advances time whenever an empty message is * enqueued at a future time. - * - *

The clock time is advanced to the time of enqueued empty messages. The first Handler sending - * messages at a future time will be allowed to advance time to ensure there is only one primary - * time source at a time. This should usually be the Handler of the internal playback loop. You can - * {@link #resetHandler() reset the handler} so that the next Handler that sends messages at a - * future time becomes the primary time source. */ public final class AutoAdvancingFakeClock extends FakeClock { - @Nullable private HandlerWrapper autoAdvancingHandler; - /** Creates the auto-advancing clock with an initial time of 0. */ public AutoAdvancingFakeClock() { this(/* initialTimeMs= */ 0); @@ -43,24 +32,6 @@ public final class AutoAdvancingFakeClock extends FakeClock { * @param initialTimeMs The initial time of the clock in milliseconds. */ public AutoAdvancingFakeClock(long initialTimeMs) { - super(initialTimeMs); - } - - @Override - protected synchronized void addPendingHandlerMessage(HandlerMessage message) { - super.addPendingHandlerMessage(message); - HandlerWrapper handler = message.getTarget(); - long currentTimeMs = elapsedRealtime(); - long messageTimeMs = message.getTimeMs(); - if (currentTimeMs < messageTimeMs - && (autoAdvancingHandler == null || autoAdvancingHandler == handler)) { - autoAdvancingHandler = handler; - advanceTime(messageTimeMs - currentTimeMs); - } - } - - /** Resets the internal handler, so that this clock can later be used with another handler. */ - public void resetHandler() { - autoAdvancingHandler = null; + super(initialTimeMs, /* isAutoAdvancing= */ true); } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index b9ee9c9f8d..8232cba48b 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -351,6 +351,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc @Nullable private final Player.EventListener eventListener; @Nullable private final AnalyticsListener analyticsListener; + private final Clock clock; private final HandlerThread playerThread; private final HandlerWrapper handler; private final CountDownLatch endedCountDownLatch; @@ -388,6 +389,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc this.actionSchedule = actionSchedule; this.eventListener = eventListener; this.analyticsListener = analyticsListener; + this.clock = playerBuilder.getClock(); timelines = new ArrayList<>(); timelineChangeReasons = new ArrayList<>(); mediaItems = new ArrayList<>(); @@ -399,8 +401,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc actionScheduleFinishedCountDownLatch = new CountDownLatch(actionSchedule != null ? 1 : 0); playerThread = new HandlerThread("ExoPlayerTest thread"); playerThread.start(); - handler = - playerBuilder.getClock().createHandler(playerThread.getLooper(), /* callback= */ null); + handler = clock.createHandler(playerThread.getLooper(), /* callback= */ null); this.pauseAtEndOfMediaItems = pauseAtEndOfMediaItems; } @@ -476,6 +477,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc * @throws Exception If any exception occurred during playback, release, or due to a timeout. */ public ExoPlayerTestRunner blockUntilEnded(long timeoutMs) throws Exception { + clock.onThreadBlocked(); if (!endedCountDownLatch.await(timeoutMs, MILLISECONDS)) { exception = new TimeoutException("Test playback timed out waiting for playback to end."); } @@ -498,6 +500,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc */ public ExoPlayerTestRunner blockUntilActionScheduleFinished(long timeoutMs) throws TimeoutException, InterruptedException { + clock.onThreadBlocked(); if (!actionScheduleFinishedCountDownLatch.await(timeoutMs, MILLISECONDS)) { throw new TimeoutException("Test playback timed out waiting for action schedule to finish."); } @@ -619,6 +622,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc playerThread.quit(); } }); + clock.onThreadBlocked(); playerThread.join(); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java index 4dea57f087..4dd32f6cc5 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.testutil; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; + import android.os.Handler; import android.os.Handler.Callback; import android.os.Looper; @@ -23,38 +25,69 @@ import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.HandlerWrapper; +import com.google.common.collect.ComparisonChain; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * Fake {@link Clock} implementation that allows to {@link #advanceTime(long) advance the time} * manually to trigger pending timed messages. * *

All timed messages sent by a {@link #createHandler(Looper, Callback) Handler} created from - * this clock are governed by the clock's time. + * this clock are governed by the clock's time. Messages sent through these handlers are not + * triggered until previous messages on any thread have been handled to ensure deterministic + * execution. Note that this includes messages sent from the main Robolectric test thread, meaning + * that these messages are only triggered if the main test thread is idle, which can be explicitly + * requested by calling {@code ShadowLooper.idleMainLooper()}. * *

The clock also sets the time of the {@link SystemClock} to match the {@link #elapsedRealtime() * clock's time}. */ public class FakeClock implements Clock { + private static long messageIdProvider = 0; + + private final boolean isAutoAdvancing; + @GuardedBy("this") private final List handlerMessages; + @GuardedBy("this") + private final Set busyLoopers; + @GuardedBy("this") private final long bootTimeMs; @GuardedBy("this") private long timeSinceBootMs; + @GuardedBy("this") + private boolean waitingForMessage; + /** - * Creates a fake clock assuming the system was booted exactly at time {@code 0} (the Unix Epoch) - * and {@code initialTimeMs} milliseconds have passed since system boot. + * Creates a fake clock that doesn't auto-advance and assumes that the system was booted exactly + * at time {@code 0} (the Unix Epoch) and {@code initialTimeMs} milliseconds have passed since + * system boot. * * @param initialTimeMs The initial elapsed time since the boot time, in milliseconds. */ public FakeClock(long initialTimeMs) { - this(/* bootTimeMs= */ 0, initialTimeMs); + this(/* bootTimeMs= */ 0, initialTimeMs, /* isAutoAdvancing= */ false); + } + + /** + * Creates a fake clock that assumes that the system was booted exactly at time {@code 0} (the + * Unix Epoch) and {@code initialTimeMs} milliseconds have passed since system boot. + * + * @param initialTimeMs The initial elapsed time since the boot time, in milliseconds. + * @param isAutoAdvancing Whether the clock should automatically advance the time to the time of + * next message that is due to be sent. + */ + public FakeClock(long initialTimeMs, boolean isAutoAdvancing) { + this(/* bootTimeMs= */ 0, initialTimeMs, isAutoAdvancing); } /** @@ -63,11 +96,15 @@ public class FakeClock implements Clock { * * @param bootTimeMs The time the system was booted since the Unix Epoch, in milliseconds. * @param initialTimeMs The initial elapsed time since the boot time, in milliseconds. + * @param isAutoAdvancing Whether the clock should automatically advance the time to the time of + * next message that is due to be sent. */ - public FakeClock(long bootTimeMs, long initialTimeMs) { + public FakeClock(long bootTimeMs, long initialTimeMs, boolean isAutoAdvancing) { this.bootTimeMs = bootTimeMs; this.timeSinceBootMs = initialTimeMs; + this.isAutoAdvancing = isAutoAdvancing; this.handlerMessages = new ArrayList<>(); + this.busyLoopers = new HashSet<>(); SystemClock.setCurrentTimeMillis(initialTimeMs); } @@ -77,9 +114,8 @@ public class FakeClock implements Clock { * @param timeDiffMs The amount of time to add to the timestamp in milliseconds. */ public synchronized void advanceTime(long timeDiffMs) { - timeSinceBootMs += timeDiffMs; - SystemClock.setCurrentTimeMillis(timeSinceBootMs); - maybeTriggerMessages(); + advanceTimeInternal(timeDiffMs); + maybeTriggerMessage(); } @Override @@ -102,10 +138,22 @@ public class FakeClock implements Clock { return new ClockHandler(looper, callback); } + @Override + public synchronized void onThreadBlocked() { + busyLoopers.add(checkNotNull(Looper.myLooper())); + waitingForMessage = false; + maybeTriggerMessage(); + } + /** Adds a message to the list of pending messages. */ protected synchronized void addPendingHandlerMessage(HandlerMessage message) { handlerMessages.add(message); - maybeTriggerMessages(); + if (!waitingForMessage) { + // If this isn't executed from inside a message created by this class, make sure the current + // looper message is finished before handling the new message. + waitingForMessage = true; + new Handler(checkNotNull(Looper.myLooper())).post(this::onMessageHandled); + } } private synchronized void removePendingHandlerMessages(ClockHandler handler, int what) { @@ -139,27 +187,65 @@ public class FakeClock implements Clock { return handler.handler.hasMessages(what); } - private synchronized void maybeTriggerMessages() { - for (int i = handlerMessages.size() - 1; i >= 0; i--) { - HandlerMessage message = handlerMessages.get(i); - if (message.timeMs <= timeSinceBootMs) { - if (message.runnable != null) { - message.handler.handler.post(message.runnable); - } else { - message - .handler - .handler - .obtainMessage(message.what, message.arg1, message.arg2, message.obj) - .sendToTarget(); - } - handlerMessages.remove(i); + private synchronized void maybeTriggerMessage() { + if (waitingForMessage) { + return; + } + if (handlerMessages.isEmpty()) { + return; + } + Collections.sort(handlerMessages); + int messageIndex = 0; + HandlerMessage message = handlerMessages.get(messageIndex); + int messageCount = handlerMessages.size(); + while (busyLoopers.contains(message.handler.getLooper()) && messageIndex < messageCount) { + messageIndex++; + if (messageIndex == messageCount) { + return; + } + message = handlerMessages.get(messageIndex); + } + if (message.timeMs > timeSinceBootMs) { + if (isAutoAdvancing) { + advanceTimeInternal(message.timeMs - timeSinceBootMs); + } else { + return; } } + handlerMessages.remove(messageIndex); + waitingForMessage = true; + if (message.runnable != null) { + message.handler.handler.post(message.runnable); + } else { + message + .handler + .handler + .obtainMessage(message.what, message.arg1, message.arg2, message.obj) + .sendToTarget(); + } + message.handler.internalHandler.post(this::onMessageHandled); + } + + private synchronized void onMessageHandled() { + busyLoopers.remove(Looper.myLooper()); + waitingForMessage = false; + maybeTriggerMessage(); + } + + private synchronized void advanceTimeInternal(long timeDiffMs) { + timeSinceBootMs += timeDiffMs; + SystemClock.setCurrentTimeMillis(timeSinceBootMs); + } + + private static synchronized long getNextMessageId() { + return messageIdProvider++; } /** Message data saved to send messages or execute runnables at a later time on a Handler. */ - protected final class HandlerMessage implements HandlerWrapper.Message { + protected final class HandlerMessage + implements Comparable, HandlerWrapper.Message { + private final long messageId; private final long timeMs; private final ClockHandler handler; @Nullable private final Runnable runnable; @@ -176,6 +262,7 @@ public class FakeClock implements Clock { int arg2, @Nullable Object obj, @Nullable Runnable runnable) { + this.messageId = getNextMessageId(); this.timeMs = timeMs; this.handler = handler; this.runnable = runnable; @@ -199,15 +286,25 @@ public class FakeClock implements Clock { public HandlerWrapper getTarget() { return handler; } + + @Override + public int compareTo(HandlerMessage other) { + return ComparisonChain.start() + .compare(this.timeMs, other.timeMs) + .compare(this.messageId, other.messageId) + .result(); + } } /** HandlerWrapper implementation using the enclosing Clock to schedule delayed messages. */ private final class ClockHandler implements HandlerWrapper { public final Handler handler; + public final Handler internalHandler; public ClockHandler(Looper looper, @Nullable Callback callback) { handler = new Handler(looper, callback); + internalHandler = new Handler(looper); } @Override @@ -311,3 +408,5 @@ public class FakeClock implements Clock { } } + + diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java index c6b39d7f27..28e57d8e66 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java @@ -30,9 +30,9 @@ import com.google.common.base.Objects; import com.google.common.collect.Iterables; import java.util.ArrayList; import java.util.List; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.shadows.ShadowLooper; /** Unit test for {@link FakeClock}. */ @RunWith(AndroidJUnit4.class) @@ -46,13 +46,16 @@ public final class FakeClockTest { @Test public void currentTimeMillis_withBootTime() { - FakeClock fakeClock = new FakeClock(/* bootTimeMs= */ 150, /* initialTimeMs= */ 200); + FakeClock fakeClock = + new FakeClock( + /* bootTimeMs= */ 150, /* initialTimeMs= */ 200, /* isAutoAdvancing= */ false); assertThat(fakeClock.currentTimeMillis()).isEqualTo(350); } @Test public void currentTimeMillis_afterAdvanceTime_currentTimeHasAdvanced() { - FakeClock fakeClock = new FakeClock(/* bootTimeMs= */ 100, /* initialTimeMs= */ 50); + FakeClock fakeClock = + new FakeClock(/* bootTimeMs= */ 100, /* initialTimeMs= */ 50, /* isAutoAdvancing= */ false); fakeClock.advanceTime(/* timeDiffMs */ 250); assertThat(fakeClock.currentTimeMillis()).isEqualTo(400); } @@ -82,6 +85,7 @@ public final class FakeClockTest { handler .obtainMessage(/* what= */ 4, /* arg1= */ 88, /* arg2= */ 33, /* obj=*/ testObject) .sendToTarget(); + ShadowLooper.idleMainLooper(); shadowOf(handler.getLooper()).idle(); assertThat(callback.messages) @@ -105,6 +109,7 @@ public final class FakeClockTest { handler.sendEmptyMessageAtTime(/* what= */ 2, /* uptimeMs= */ fakeClock.uptimeMillis() + 60); handler.sendEmptyMessageDelayed(/* what= */ 3, /* delayMs= */ 50); handler.sendEmptyMessage(/* what= */ 4); + ShadowLooper.idleMainLooper(); shadowOf(handler.getLooper()).idle(); assertThat(callback.messages) @@ -128,8 +133,6 @@ public final class FakeClockTest { .isEqualTo(new MessageData(/* what= */ 2, /* arg1= */ 0, /* arg2= */ 0, /* obj=*/ null)); } - // Temporarily disabled until messages are ordered correctly. - @Ignore @Test public void createHandler_sendMessageAtFrontOfQueue_sendsMessageFirst() { HandlerThread handlerThread = new HandlerThread("FakeClockTest"); @@ -141,6 +144,7 @@ public final class FakeClockTest { handler.obtainMessage(/* what= */ 1).sendToTarget(); handler.sendMessageAtFrontOfQueue(handler.obtainMessage(/* what= */ 2)); handler.obtainMessage(/* what= */ 3).sendToTarget(); + ShadowLooper.idleMainLooper(); shadowOf(handler.getLooper()).idle(); assertThat(callback.messages) @@ -169,12 +173,14 @@ public final class FakeClockTest { handler.postDelayed(testRunnables[0], 0); handler.postDelayed(testRunnables[1], 100); handler.postDelayed(testRunnables[2], 200); + ShadowLooper.idleMainLooper(); shadowOf(handler.getLooper()).idle(); assertTestRunnableStates(new boolean[] {true, false, false, false, false}, testRunnables); fakeClock.advanceTime(150); handler.postDelayed(testRunnables[3], 50); handler.postDelayed(testRunnables[4], 100); + ShadowLooper.idleMainLooper(); shadowOf(handler.getLooper()).idle(); assertTestRunnableStates(new boolean[] {true, true, false, false, false}, testRunnables); @@ -197,9 +203,6 @@ public final class FakeClockTest { TestCallback otherCallback = new TestCallback(); HandlerWrapper otherHandler = fakeClock.createHandler(handlerThread.getLooper(), otherCallback); - // Block any further execution on the HandlerThread until we had a chance to remove messages. - ConditionVariable startCondition = new ConditionVariable(); - handler.post(startCondition::block); TestRunnable testRunnable1 = new TestRunnable(); TestRunnable testRunnable2 = new TestRunnable(); Object messageToken = new Object(); @@ -213,8 +216,8 @@ public final class FakeClockTest { handler.removeMessages(/* what= */ 2); handler.removeCallbacksAndMessages(messageToken); - startCondition.open(); fakeClock.advanceTime(50); + ShadowLooper.idleMainLooper(); shadowOf(handlerThread.getLooper()).idle(); assertThat(callback.messages) @@ -239,9 +242,6 @@ public final class FakeClockTest { TestCallback otherCallback = new TestCallback(); HandlerWrapper otherHandler = fakeClock.createHandler(handlerThread.getLooper(), otherCallback); - // Block any further execution on the HandlerThread until we had a chance to remove messages. - ConditionVariable startCondition = new ConditionVariable(); - handler.post(startCondition::block); TestRunnable testRunnable1 = new TestRunnable(); TestRunnable testRunnable2 = new TestRunnable(); Object messageToken = new Object(); @@ -254,8 +254,8 @@ public final class FakeClockTest { handler.removeCallbacksAndMessages(/* token= */ null); - startCondition.open(); fakeClock.advanceTime(50); + ShadowLooper.idleMainLooper(); shadowOf(handlerThread.getLooper()).idle(); assertThat(callback.messages).isEmpty(); @@ -268,6 +268,109 @@ public final class FakeClockTest { new MessageData(/* what= */ 1, /* arg1= */ 0, /* arg2= */ 0, /* obj=*/ null)); } + @Test + public void createHandler_withIsAutoAdvancing_advancesTimeToNextMessages() { + HandlerThread handlerThread = new HandlerThread("FakeClockTest"); + handlerThread.start(); + FakeClock fakeClock = new FakeClock(/* initialTimeMs= */ 0, /* isAutoAdvancing= */ true); + HandlerWrapper handler = + fakeClock.createHandler(handlerThread.getLooper(), /* callback= */ null); + + // Post a series of immediate and delayed messages. + ArrayList clockTimes = new ArrayList<>(); + handler.post( + () -> { + handler.postDelayed( + () -> clockTimes.add(fakeClock.elapsedRealtime()), /* delayMs= */ 100); + handler.postDelayed(() -> clockTimes.add(fakeClock.elapsedRealtime()), /* delayMs= */ 50); + handler.post(() -> clockTimes.add(fakeClock.elapsedRealtime())); + handler.postDelayed( + () -> { + clockTimes.add(fakeClock.elapsedRealtime()); + handler.postDelayed( + () -> clockTimes.add(fakeClock.elapsedRealtime()), /* delayMs= */ 50); + }, + /* delayMs= */ 20); + }); + ShadowLooper.idleMainLooper(); + shadowOf(handler.getLooper()).idle(); + + assertThat(clockTimes).containsExactly(0L, 20L, 50L, 70L, 100L).inOrder(); + } + + @Test + public void createHandler_multiThreadCommunication_deliversMessagesDeterministicallyInOrder() { + HandlerThread handlerThread1 = new HandlerThread("FakeClockTest"); + handlerThread1.start(); + HandlerThread handlerThread2 = new HandlerThread("FakeClockTest"); + handlerThread2.start(); + FakeClock fakeClock = new FakeClock(/* initialTimeMs= */ 0); + HandlerWrapper handler1 = + fakeClock.createHandler(handlerThread1.getLooper(), /* callback= */ null); + HandlerWrapper handler2 = + fakeClock.createHandler(handlerThread2.getLooper(), /* callback= */ null); + + ConditionVariable messagesFinished = new ConditionVariable(); + ArrayList executionOrder = new ArrayList<>(); + handler1.post( + () -> { + executionOrder.add(1); + handler2.post(() -> executionOrder.add(2)); + handler1.post(() -> executionOrder.add(3)); + handler2.post( + () -> { + executionOrder.add(4); + handler2.post(() -> executionOrder.add(7)); + handler1.post( + () -> { + executionOrder.add(8); + messagesFinished.open(); + }); + }); + handler2.post(() -> executionOrder.add(5)); + handler1.post(() -> executionOrder.add(6)); + }); + ShadowLooper.idleMainLooper(); + messagesFinished.block(); + + assertThat(executionOrder).containsExactly(1, 2, 3, 4, 5, 6, 7, 8).inOrder(); + } + + @Test + public void createHandler_blockingThreadWithOnBusyWaiting_canBeUnblockedByOtherThread() { + HandlerThread handlerThread1 = new HandlerThread("FakeClockTest"); + handlerThread1.start(); + HandlerThread handlerThread2 = new HandlerThread("FakeClockTest"); + handlerThread2.start(); + FakeClock fakeClock = new FakeClock(/* initialTimeMs= */ 0, /* isAutoAdvancing= */ true); + HandlerWrapper handler1 = + fakeClock.createHandler(handlerThread1.getLooper(), /* callback= */ null); + HandlerWrapper handler2 = + fakeClock.createHandler(handlerThread2.getLooper(), /* callback= */ null); + + ArrayList executionOrder = new ArrayList<>(); + handler1.post( + () -> { + executionOrder.add(1); + ConditionVariable blockingCondition = new ConditionVariable(); + handler2.postDelayed( + () -> { + executionOrder.add(2); + blockingCondition.open(); + }, + /* delayMs= */ 50); + handler1.post(() -> executionOrder.add(4)); + fakeClock.onThreadBlocked(); + blockingCondition.block(); + executionOrder.add(3); + }); + ShadowLooper.idleMainLooper(); + shadowOf(handler1.getLooper()).idle(); + shadowOf(handler2.getLooper()).idle(); + + assertThat(executionOrder).containsExactly(1, 2, 3, 4).inOrder(); + } + private static void assertTestRunnableStates(boolean[] states, TestRunnable[] testRunnables) { for (int i = 0; i < testRunnables.length; i++) { assertThat(testRunnables[i].hasRun).isEqualTo(states[i]);