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]);