diff --git a/gradle.properties b/gradle.properties index 7ed60456a8b..0bed8c8b057 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ errorproneJavacVersion=9+181-r4173-1 # AndroidX test versions axtVersion=1.4.0 -espressoVersion=3.5.0-alpha04 +espressoVersion=3.5.0-alpha05 axtJunitVersion=1.1.3 # AndroidX versions diff --git a/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/EspressoIdlingResourceTest.java b/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/EspressoIdlingResourceTest.java new file mode 100644 index 00000000000..36565a24a75 --- /dev/null +++ b/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/EspressoIdlingResourceTest.java @@ -0,0 +1,181 @@ +package org.robolectric.integrationtests.axt; + +import static androidx.test.espresso.Espresso.onIdle; +import static com.google.common.truth.Truth.assertThat; + +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import androidx.test.espresso.IdlingRegistry; +import androidx.test.espresso.IdlingResource; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Test Espresso IdlingResource support. */ +@RunWith(AndroidJUnit4.class) +public final class EspressoIdlingResourceTest { + private final IdlingRegistry idlingRegistry = IdlingRegistry.getInstance(); + + private ExecutorService executor; + + @Before + public void setup() { + executor = Executors.newSingleThreadExecutor(); + } + + @After + public void teardown() { + for (IdlingResource resource : idlingRegistry.getResources()) { + idlingRegistry.unregister(resource); + } + for (Looper looper : idlingRegistry.getLoopers()) { + idlingRegistry.unregisterLooperAsIdlingResource(looper); + } + executor.shutdown(); + } + + @Test + public void onIdle_idlingResourceIsIdle_doesntBlock() { + AtomicBoolean didCheckIdle = new AtomicBoolean(); + idlingRegistry.register( + new NamedIdleResource("Test", /* isIdle= */ true) { + @Override + public boolean isIdleNow() { + didCheckIdle.set(true); + return super.isIdleNow(); + } + }); + + onIdle(); + + assertThat(didCheckIdle.get()).isTrue(); + } + + @SuppressWarnings("FutureReturnValueIgnored") + @Test + public void onIdle_postToMainThread() { + idlingRegistry.register( + new NamedIdleResource("Test", /* isIdle= */ false) { + boolean submitted; + + @Override + public boolean isIdleNow() { + if (!submitted) { + submitted = true; + executor.submit(this::postToMainLooper); + } + return super.isIdleNow(); + } + + void postToMainLooper() { + new Handler(Looper.getMainLooper()).post(() -> setIdle(true)); + } + }); + + onIdle(); + } + + @SuppressWarnings("FutureReturnValueIgnored") + @Test + public void onIdle_cooperativeResources() { + NamedIdleResource a = new NamedIdleResource("A", /* isIdle= */ true); + NamedIdleResource b = new NamedIdleResource("B", /* isIdle= */ false); + NamedIdleResource c = new NamedIdleResource("C", /* isIdle= */ false); + idlingRegistry.register(a, b); + executor.submit( + () -> { + a.setIdle(false); + b.setIdle(true); + c.setIdle(false); + executor.submit( + () -> { + a.setIdle(true); + b.setIdle(false); + c.setIdle(false); + executor.submit( + () -> { + a.setIdle(true); + b.setIdle(true); + c.setIdle(true); + }); + }); + }); + + onIdle(); + + assertThat(a.isIdleNow()).isTrue(); + assertThat(b.isIdleNow()).isTrue(); + assertThat(c.isIdleNow()).isTrue(); + } + + @SuppressWarnings("FutureReturnValueIgnored") + @Test + public void onIdle_looperIsIdle() throws Exception { + HandlerThread handlerThread = new HandlerThread("Test"); + try { + handlerThread.start(); + Handler handler = new Handler(handlerThread.getLooper()); + CountDownLatch handlerStarted = new CountDownLatch(1); + CountDownLatch releaseHandler = new CountDownLatch(1); + handler.post( + () -> { + handlerStarted.countDown(); + try { + releaseHandler.await(); + } catch (InterruptedException e) { + // ignore + } + }); + handlerStarted.await(); + idlingRegistry.registerLooperAsIdlingResource(handlerThread.getLooper()); + + executor.submit(releaseHandler::countDown); + onIdle(); + + // onIdle should have blocked on the looper waiting on the release latch + assertThat(releaseHandler.getCount()).isEqualTo(0); + } finally { + handlerThread.quit(); + } + } + + private static class NamedIdleResource implements IdlingResource { + final String name; + final AtomicBoolean isIdle; + ResourceCallback callback; + + NamedIdleResource(String name, boolean isIdle) { + this.name = name; + this.isIdle = new AtomicBoolean(isIdle); + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean isIdleNow() { + return isIdle.get(); + } + + void setIdle(boolean isIdle) { + this.isIdle.set(isIdle); + if (isIdle && callback != null) { + callback.onTransitionToIdle(); + } + } + + @Override + public void registerIdleTransitionCallback(ResourceCallback callback) { + this.callback = callback; + } + } +} diff --git a/robolectric/build.gradle b/robolectric/build.gradle index 13355388b24..96484372cb7 100755 --- a/robolectric/build.gradle +++ b/robolectric/build.gradle @@ -44,6 +44,7 @@ dependencies { compileOnly AndroidSdk.MAX_SDK.coordinates compileOnly "junit:junit:${junitVersion}" implementation "androidx.test:monitor:$axtVersion" + implementation "androidx.test.espresso:espresso-idling-resource:3.4.0" testImplementation "junit:junit:${junitVersion}" testImplementation "com.google.truth:truth:${truthVersion}" diff --git a/robolectric/src/main/java/org/robolectric/android/internal/IdlingResourceTimeoutException.java b/robolectric/src/main/java/org/robolectric/android/internal/IdlingResourceTimeoutException.java new file mode 100644 index 00000000000..29de83dec2d --- /dev/null +++ b/robolectric/src/main/java/org/robolectric/android/internal/IdlingResourceTimeoutException.java @@ -0,0 +1,24 @@ +package org.robolectric.android.internal; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.annotations.Beta; +import java.util.List; +import java.util.Locale; + +/** + * Timeout exception thrown when idling resources are not idle for longer than the configured + * timeout. + * + *

See {@link androidx.test.espresso.IdlingResourceTimeoutException}. + * + *

Note: This API may be removed in the future in favor of using espresso's exception directly. + */ +@Beta +public final class IdlingResourceTimeoutException extends RuntimeException { + public IdlingResourceTimeoutException(List resourceNames) { + super( + String.format( + Locale.ROOT, "Wait for %s to become idle timed out", checkNotNull(resourceNames))); + } +} diff --git a/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java b/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java index f73b0057e3d..f20e3e50842 100644 --- a/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java +++ b/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java @@ -7,12 +7,16 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static java.util.Comparator.comparingInt; +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.stream.Collectors.toList; +import static org.robolectric.Shadows.shadowOf; import static org.robolectric.shadows.ShadowLooper.shadowMainLooper; import android.annotation.SuppressLint; import android.os.Build; import android.os.Build.VERSION_CODES; +import android.os.Handler; import android.os.Looper; import android.os.SystemClock; import android.util.Log; @@ -23,15 +27,31 @@ import android.view.WindowManager.LayoutParams; import android.view.WindowManagerGlobal; import android.view.WindowManagerImpl; +import androidx.test.espresso.IdlingRegistry; +import androidx.test.espresso.IdlingResource; import androidx.test.platform.ui.InjectEventSecurityException; import androidx.test.platform.ui.UiController; +import com.google.common.annotations.Beta; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.LooperMode; +import org.robolectric.shadow.api.Shadow; +import org.robolectric.shadows.ShadowLooper; +import org.robolectric.shadows.ShadowPausedLooper; import org.robolectric.util.ReflectionHelpers; /** A {@link UiController} that runs on a local JVM with Robolectric. */ @@ -46,6 +66,22 @@ public class LocalUiController implements UiController { private static final Predicate WATCH_TOUCH_OUTSIDE = IS_TOUCH_MODAL.negate().and(hasLayoutFlag(FLAG_WATCH_OUTSIDE_TOUCH)); + private static long idlingResourceErrorTimeoutMs = SECONDS.toMillis(26); + private final HashSet syncedIdlingResources = new HashSet<>(); + private final ExecutorService looperIdlingExecutor = Executors.newCachedThreadPool(); + + /** + * Sets the error timeout for idling resources. + * + *

See {@link androidx.test.espresso.IdlingPolicies#setIdlingResourceTimeout(long, TimeUnit)}. + * + *

Note: This API may be removed in the future in favor of using IdlingPolicies directly. + */ + @Beta + public static void setIdlingResourceTimeout(long timeout, TimeUnit unit) { + idlingResourceErrorTimeoutMs = unit.toMillis(timeout); + } + @Override public boolean injectMotionEvent(MotionEvent event) throws InjectEventSecurityException { checkNotNull(event); @@ -174,7 +210,110 @@ static KeyCharacterMap getKeyCharacterMap() { @Override public void loopMainThreadUntilIdle() { - shadowMainLooper().idle(); + if (!ShadowLooper.looperMode().equals(LooperMode.Mode.PAUSED)) { + shadowMainLooper().idle(); + } else { + ImmutableSet idlingResources = syncIdlingResources(); + if (idlingResources.isEmpty()) { + shadowMainLooper().idle(); + } else { + loopMainThreadUntilIdlingResourcesIdle(idlingResources); + } + } + } + + private void loopMainThreadUntilIdlingResourcesIdle( + ImmutableSet idlingResources) { + Looper mainLooper = Looper.myLooper(); + ShadowPausedLooper shadowMainLooper = Shadow.extract(mainLooper); + Handler handler = new Handler(mainLooper); + Set activeResources = new HashSet<>(); + long startTimeNanos = System.nanoTime(); + + shadowMainLooper.idle(); + while (true) { + // Gather the list of resources that are not idling. + for (IdlingResourceProxy resource : idlingResources) { + // Add the resource as active and check if it's idle, if it is already is will be removed + // synchronously. The idle callback is synchronized in the resource which avoids a race + // between registering the idle callback and checking the idle state. + activeResources.add(resource); + resource.notifyOnIdle( + () -> { + if (Looper.myLooper() == mainLooper) { + activeResources.remove(resource); + } else { + // Post to restart the main thread. + handler.post(() -> activeResources.remove(resource)); + } + }); + } + // If all are idle then just return, we're done. + if (activeResources.isEmpty()) { + break; + } + // While the resources that weren't idle haven't transitioned to idle continue to loop the + // main looper waiting for any new messages. Once all resources have transitioned to idle loop + // around again to make sure all resources are idle at the same time. + while (!activeResources.isEmpty()) { + long elapsedTimeMs = NANOSECONDS.toMillis(System.nanoTime() - startTimeNanos); + if (elapsedTimeMs >= idlingResourceErrorTimeoutMs) { + throw new IdlingResourceTimeoutException(idlingResourceNames(activeResources)); + } + // Poll the queue and suspend the thread until we get new messages or the idle transition. + shadowMainLooper.poll(idlingResourceErrorTimeoutMs - elapsedTimeMs); + shadowMainLooper.idle(); + } + } + } + + private ImmutableSet syncIdlingResources() { + // Collect unique registered idling resources. + HashMap registeredResourceByName = new HashMap<>(); + for (IdlingResource resource : IdlingRegistry.getInstance().getResources()) { + String name = resource.getName(); + if (registeredResourceByName.containsKey(name)) { + logDuplicate(name, registeredResourceByName.get(name), resource); + } else { + registeredResourceByName.put(name, resource); + } + } + Iterator iterator = syncedIdlingResources.iterator(); + while (iterator.hasNext()) { + IdlingResourceProxyImpl proxy = iterator.next(); + if (registeredResourceByName.get(proxy.name) == proxy.resource) { + // Already registered, don't need to add. + registeredResourceByName.remove(proxy.name); + } else { + // Previously registered, but no longer registered, remove. + iterator.remove(); + } + } + // Add new idling resources that weren't previously registered. + for (Map.Entry entry : registeredResourceByName.entrySet()) { + syncedIdlingResources.add(new IdlingResourceProxyImpl(entry.getKey(), entry.getValue())); + } + + return ImmutableSet.builder() + .addAll(syncedIdlingResources) + .addAll( + IdlingRegistry.getInstance().getLoopers().stream() + .map(LooperIdlingResource::new) + .iterator()) + .build(); + } + + private static void logDuplicate(String name, IdlingResource a, IdlingResource b) { + Log.e( + TAG, + String.format( + "Attempted to register resource with same names:" + + " %s. R1: %s R2: %s.\nDuplicate resource registration will be ignored.", + name, a, b)); + } + + private static List idlingResourceNames(Set idlingResources) { + return idlingResources.stream().map(IdlingResourceProxy::getName).collect(toList()); } @Override @@ -275,4 +414,88 @@ boolean watchTouchOutside() { return WATCH_TOUCH_OUTSIDE.test(this); } } + + private interface IdlingResourceProxy { + String getName(); + + void notifyOnIdle(Runnable idleCallback); + } + + private static final class IdlingResourceProxyImpl implements IdlingResourceProxy { + private final String name; + private final IdlingResource resource; + + private Runnable idleCallback; + + IdlingResourceProxyImpl(String name, IdlingResource resource) { + this.name = name; + this.resource = resource; + resource.registerIdleTransitionCallback(this::onIdle); + } + + @Override + public String getName() { + return this.name; + } + + @Override + public synchronized void notifyOnIdle(Runnable idleCallback) { + if (resource.isIdleNow()) { + this.idleCallback = null; + idleCallback.run(); + } else { + this.idleCallback = idleCallback; + } + } + + private synchronized void onIdle() { + if (idleCallback != null) { + idleCallback.run(); + idleCallback = null; + } + } + } + + private final class LooperIdlingResource implements IdlingResourceProxy { + private final Looper looper; + private final ShadowLooper shadowLooper; + private Runnable idleCallback; + + LooperIdlingResource(Looper looper) { + this.looper = looper; + this.shadowLooper = shadowOf(looper); + } + + @Override + public String getName() { + return looper.toString(); + } + + @Override + public synchronized void notifyOnIdle(Runnable idleCallback) { + if (shadowLooper.isIdle()) { + this.idleCallback = null; + idleCallback.run(); + } else { + this.idleCallback = idleCallback; + // Note idle() doesn't throw an exception if called from another thread, the looper would + // die with an unhandled exception. + // TODO(paulsowden): It's not technically necessary to idle the looper from another thread, + // it can be idled from its own thread, however we'll need API access to do this and + // observe the idle state--the idle() api blocks the calling thread by default. Perhaps a + // ListenableFuture idleAsync() variant? + looperIdlingExecutor.execute(this::idleLooper); + } + } + + private void idleLooper() { + shadowLooper.idle(); + synchronized (this) { + if (idleCallback != null) { + idleCallback.run(); + idleCallback = null; + } + } + } + } } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java index 1e23ef7aa23..cadc3e96952 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java @@ -25,6 +25,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -544,6 +545,26 @@ public void testIdleNotStuck_whenThreadCrashes() throws Exception { assertThat(thread.getState()).isEqualTo(Thread.State.TERMINATED); } + @Test + public void poll() { + ShadowPausedLooper shadowPausedLooper = Shadow.extract(Looper.getMainLooper()); + AtomicBoolean backgroundThreadPosted = new AtomicBoolean(); + AtomicBoolean foregroundThreadReceived = new AtomicBoolean(); + shadowPausedLooper.idle(); + + new Handler(handlerThread.getLooper()) + .post( + () -> { + backgroundThreadPosted.set(true); + new Handler(Looper.getMainLooper()).post(() -> foregroundThreadReceived.set(true)); + }); + shadowPausedLooper.poll(0); + shadowPausedLooper.idle(); + + assertThat(backgroundThreadPosted.get()).isTrue(); + assertThat(foregroundThreadReceived.get()).isTrue(); + } + private static class BlockingRunnable implements Runnable { CountDownLatch latch = new CountDownLatch(1); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java index b2bf2322f02..df4e15b583f 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java @@ -1,5 +1,6 @@ package org.robolectric.shadows; +import static com.google.common.base.Preconditions.checkState; import static org.robolectric.shadow.api.Shadow.invokeConstructor; import static org.robolectric.util.ReflectionHelpers.ClassParameter.from; import static org.robolectric.util.reflector.Reflector.reflector; @@ -206,6 +207,41 @@ public void runPaused(Runnable runnable) { } } + /** + * Polls the message queue waiting until a message is posted to the head of the queue. This will + * suspend the thread until a new message becomes available. Returns immediately if the queue is + * not idle. There's no guarantee that the message queue will not still be idle when returning, + * but if the message queue becomes not idle it will return immediately. + * + *

This method is only applicable for the main looper's queue when called on the main thread, + * as the main looper in Robolectric is processed manually (it doesn't loop)--looper threads are + * using the native polling of their loopers. Throws an exception if called for another looper's + * queue. Non-main thread loopers should use {@link #unPause()}. + * + *

This should be used with care, it can be used to suspend the main (i.e. test) thread while + * worker threads perform some work, and then resumed by posting to the main looper. Used in a + * loop to wait on some condition it can process messages on the main looper, simulating the + * behavior of the real looper, for example: + * + *

{@code
+   * while (!condition) {
+   *   shadowMainLooper.poll(timeout);
+   *   shadowMainLooper.idle();
+   * }
+   * }
+ * + *

Beware though that a message must be posted to the main thread after the condition is + * satisfied, or the condition satisfied while idling the main thread, otherwise the main thread + * will continue to be suspended until the timeout. + * + * @param timeout Timeout in milliseconds, the maximum time to wait before returning, or 0 to wait + * indefinitely, + */ + public void poll(long timeout) { + checkState(Looper.myLooper() == Looper.getMainLooper() && Looper.myLooper() == realLooper); + shadowQueue().poll(timeout); + } + @Override public Duration getNextScheduledTaskTime() { return shadowQueue().getNextScheduledTaskTime(); @@ -344,7 +380,7 @@ private class RunOneRunnable extends ControlRunnable { @Override public void run() { try { - Message msg = shadowQueue().poll(); + Message msg = shadowQueue().getNextIgnoringWhen(); if (msg != null) { SystemClock.setCurrentTimeMillis(shadowMsg(msg).getWhen()); msg.getTarget().dispatchMessage(msg); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java index 2c378663f70..8ddcc7bc229 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java @@ -6,10 +6,12 @@ import static android.os.Build.VERSION_CODES.KITKAT_WATCH; import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1; import static android.os.Build.VERSION_CODES.M; +import static com.google.common.base.Preconditions.checkState; import static org.robolectric.shadow.api.Shadow.invokeConstructor; import static org.robolectric.util.ReflectionHelpers.ClassParameter.from; import static org.robolectric.util.reflector.Reflector.reflector; +import android.os.Looper; import android.os.Message; import android.os.MessageQueue; import android.os.MessageQueue.IdleHandler; @@ -23,6 +25,7 @@ import org.robolectric.annotation.RealObject; import org.robolectric.res.android.NativeObjRegistry; import org.robolectric.shadow.api.Shadow; +import org.robolectric.util.ReflectionHelpers; import org.robolectric.util.Scheduler; import org.robolectric.util.reflector.Accessor; import org.robolectric.util.reflector.Direct; @@ -33,6 +36,7 @@ * *

This class should not be referenced directly. Use {@link ShadowMessageQueue} instead. */ +@SuppressWarnings("SynchronizeOnNonFinalField") @Implements(value = MessageQueue.class, isInAndroidSdk = false, looseSignatures = true) public class ShadowPausedMessageQueue extends ShadowMessageQueue { @@ -104,6 +108,40 @@ protected void nativePollOnce(long ptr, int timeoutMillis) { } } + /** + * Polls the message queue waiting until a message is posted to the head of the queue. This will + * suspend the thread until a new message becomes available. Returns immediately if the queue is + * not idle. There's no guarantee that the message queue will not still be idle when returning, + * but if the message queue becomes not idle it will return immediately. + * + *

See {@link ShadowPausedLooper#poll(long)} for more information. + * + * @param timeout Timeout in milliseconds, the maximum time to wait before returning, or 0 to wait + * indefinitely, + */ + void poll(long timeout) { + checkState(Looper.myLooper() == Looper.getMainLooper() && Looper.myQueue() == realQueue); + // Message queue typically expects the looper to loop calling next() which returns current + // messages from the head of the queue. If no messages are current it will mark itself blocked + // and call nativePollOnce (see above) which suspends the thread until the next message's time. + // When messages are posted to the queue, if a new message is posted to the head and the queue + // is marked as blocked, then the enqueue function will notify and resume next(), allowing it + // return the next message. To simulate this behavior check if the queue is idle and if it is + // mark the queue as blocked and wait on a new message. + synchronized (realQueue) { + if (isIdle()) { + ReflectionHelpers.setField(realQueue, "mBlocked", true); + try { + realQueue.wait(timeout); + } catch (InterruptedException ignored) { + // Fall through and unblock with no messages. + } finally { + ReflectionHelpers.setField(realQueue, "mBlocked", false); + } + } + } + } + @Implementation(maxSdk = JELLY_BEAN_MR1) protected void nativeWake(int ptr) { synchronized (realQueue) { @@ -272,7 +310,12 @@ public int internalGetSize() { return count; } - Message poll() { + /** + * Returns the message at the head of the queue immediately, regardless of its scheduled time. + * Compare to {@link #getNext()} which will only return the next message if the system clock is + * advanced to its scheduled time. + */ + Message getNextIgnoringWhen() { synchronized (realQueue) { Message head = getMessages(); if (head != null) {