diff --git a/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/RobolectricUtil.java b/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/RobolectricUtil.java index bff6778b3f..c814bf3ce0 100644 --- a/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/RobolectricUtil.java +++ b/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/RobolectricUtil.java @@ -63,6 +63,12 @@ public final class RobolectricUtil { * *

Must be called on the main test thread. * + *

Note for {@link androidx.media3.test.utils.FakeClock} users: If the condition changes + * outside of a main {@link Looper} message, for example because it's checking a volatile variable + * or shared synchronized state that is updated on a background thread, or because checking the + * condition itself may cause it to become true, then the remainder of the test method may be + * executed in parallel with other background thread messages. + * * @param condition The condition. * @throws TimeoutException If the {@link #DEFAULT_TIMEOUT_MS} is exceeded. */ @@ -76,6 +82,12 @@ public final class RobolectricUtil { * *

Must be called on the main test thread. * + *

Note for {@link androidx.media3.test.utils.FakeClock} users: If the condition changes + * outside of a main {@link Looper} message, for example because it's checking a volatile variable + * or shared synchronized state that is updated on a background thread, or because checking the + * condition itself may cause it to become true, then the remainder of the test method may be + * executed in parallel with other background thread messages. + * * @param condition The condition. * @param timeoutMs The timeout in milliseconds. * @param clock The {@link Clock} to measure the timeout. @@ -91,6 +103,12 @@ public final class RobolectricUtil { * *

Must be called on the thread corresponding to the {@code looper}. * + *

Note for {@link androidx.media3.test.utils.FakeClock} users: If the condition changes + * outside of a message on this {@code Looper}, for example because it's checking a volatile + * variable or shared synchronized state that is updated on a background thread, or because + * checking the condition itself may cause it to become true, then the remainder of the test + * method may be executed in parallel with other background thread messages. + * * @param looper The {@link Looper}. * @param condition The condition. * @throws TimeoutException If the {@link #DEFAULT_TIMEOUT_MS} is exceeded. @@ -105,6 +123,12 @@ public final class RobolectricUtil { * *

Must be called on the thread corresponding to the {@code looper}. * + *

Note for {@link androidx.media3.test.utils.FakeClock} users: If the condition changes + * outside of a message on this {@code Looper}, for example because it's checking a volatile + * variable or shared synchronized state that is updated on a background thread, or because + * checking the condition itself may cause it to become true, then the remainder of the test + * method may be executed in parallel with other background thread messages. + * * @param looper The {@link Looper}. * @param condition The condition. * @param timeoutMs The timeout in milliseconds. diff --git a/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java b/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java index d9106acdbe..b28f4a92ee 100644 --- a/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java +++ b/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java @@ -26,6 +26,7 @@ import androidx.media3.common.PlaybackException; import androidx.media3.common.Player; import androidx.media3.common.Timeline; import androidx.media3.common.util.ConditionVariable; +import androidx.media3.common.util.HandlerWrapper; import androidx.media3.common.util.NullableType; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; @@ -411,6 +412,47 @@ public final class TestPlayerRunHelper { runMainLooperUntil(receivedMessageCallback::get); } + /** + * Runs tasks of the main {@link Looper} until the specified condition becomes true independent + * of a message on the main {@link Looper}. + * + *

This method is useful for cases where the condition may change outside of a main {@link + * Looper} message, for example because it's checking a volatile variable or shared synchronized + * state that is updated on a background thread, or because checking the condition itself may + * cause it to become true. + * + *

This method ensures the condition is checked within artificially created main {@link + * Looper} messages. When using a {@link androidx.media3.test.utils.FakeClock}, this guarantees + * the remainder of the test method is not executed in parallel with other background thread + * messages. + * + * @param backgroundThreadCondition The condition to wait for. + * @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is + * exceeded. + */ + public void untilBackgroundThreadCondition(Supplier backgroundThreadCondition) + throws Exception { + if (backgroundThreadCondition.get()) { + return; + } + AtomicBoolean conditionTrue = new AtomicBoolean(); + HandlerWrapper handler = + player.getClock().createHandler(Util.getCurrentOrMainLooper(), /* callback= */ null); + Runnable checkCondition = + new Runnable() { + @Override + public void run() { + if (backgroundThreadCondition.get()) { + conditionTrue.set(true); + } else { + handler.postDelayed(this, /* delayMs= */ 1); + } + } + }; + handler.post(checkCondition); + runUntil(conditionTrue::get); + } + @Override public ExoPlayerRunResult ignoringNonFatalErrors() { checkState(!hasBeenUsed);