aboutsummaryrefslogtreecommitdiff
path: root/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java
diff options
context:
space:
mode:
Diffstat (limited to 'shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java')
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java119
1 files changed, 93 insertions, 26 deletions
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 3114b11c5..aa63a6872 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java
@@ -6,7 +6,6 @@ import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
import static org.robolectric.util.reflector.Reflector.reflector;
import android.app.Instrumentation;
-import android.os.Build;
import android.os.ConditionVariable;
import android.os.Handler;
import android.os.Looper;
@@ -14,6 +13,7 @@ import android.os.Message;
import android.os.MessageQueue.IdleHandler;
import android.os.SystemClock;
import android.util.Log;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import java.time.Duration;
import java.util.ArrayList;
@@ -79,7 +79,7 @@ public final class ShadowPausedLooper extends ShadowLooper {
invokeConstructor(Looper.class, realLooper, from(boolean.class, quitAllowed));
loopingLoopers.add(realLooper);
- looperExecutor = new HandlerExecutor(new Handler(realLooper));
+ looperExecutor = new HandlerExecutor(realLooper);
}
protected static Collection<Looper> getLoopers() {
@@ -218,6 +218,22 @@ public final class ShadowPausedLooper extends ShadowLooper {
executeOnLooper(new PostAndIdleToRunnable(runnable));
}
+ /**
+ * Posts the runnable as an asynchronous task and wait until it has been run. Ignores all
+ * exceptions.
+ *
+ * <p>This method is similar to postSync, but used in internal cases where you want to make a best
+ * effort quick attempt to execute the Runnable, and do not need to idle all the non-async tasks
+ * that might be posted to the Looper's queue.
+ */
+ void postSyncQuiet(Runnable runnable) {
+ try {
+ executeOnLooper(new PostAsyncAndIdleToRunnable(runnable));
+ } catch (RuntimeException e) {
+ Log.w("ShadowPausedLooper", "ignoring exception on postSyncQuiet", e);
+ }
+ }
+
// this API doesn't make sense in LooperMode.PAUSED, but just retain it for backwards
// compatibility for now
@Override
@@ -291,6 +307,7 @@ public final class ShadowPausedLooper extends ShadowLooper {
ShadowPausedLooper shadowPausedLooper = Shadow.extract(looper);
shadowPausedLooper.resetLooperToInitialState();
}
+ ShadowPausedChoreographer.clearLoopers();
}
private static final Object instrumentationTestMainThreadLock = new Object();
@@ -359,20 +376,23 @@ public final class ShadowPausedLooper extends ShadowLooper {
}
}
- private synchronized void resetLooperToInitialState() {
+ @VisibleForTesting
+ synchronized void resetLooperToInitialState() {
// Do not use looperMode() here, because its cached value might already have been reset
LooperMode.Mode looperMode = ConfigurationRegistry.get(LooperMode.Mode.class);
ShadowPausedMessageQueue shadowQueue = Shadow.extract(realLooper.getQueue());
shadowQueue.reset();
- boolean canBeUnpaused =
- !(realLooper == Looper.getMainLooper()
- && looperMode != LooperMode.Mode.INSTRUMENTATION_TEST);
- if (canBeUnpaused && realLooper.getThread().isAlive()) {
- if (isPaused()) {
- unPause();
- }
+ if (realLooper.getThread().isAlive()
+ && !shadowQueue
+ .isQuitting()) { // Trying to unpause a quitted background Looper may deadlock.
+
+ if (isPaused()
+ && !(realLooper == Looper.getMainLooper() && looperMode != Mode.INSTRUMENTATION_TEST)) {
+ unPause();
+ }
+ ShadowPausedChoreographer.reset(realLooper);
}
}
@@ -388,10 +408,13 @@ public final class ShadowPausedLooper extends ShadowLooper {
if (isPaused()) {
executeOnLooper(new UnPauseRunnable());
}
- reflector(LooperReflector.class, realLooper).quit();
+ synchronized (realLooper.getQueue()) {
+ drainQueueSafely(shadowQueue());
+ reflector(LooperReflector.class, realLooper).quit();
+ }
}
- @Implementation(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR2)
+ @Implementation
protected void quitSafely() {
if (isPaused()) {
executeOnLooper(new UnPauseRunnable());
@@ -457,15 +480,7 @@ public final class ShadowPausedLooper extends ShadowLooper {
} else {
synchronized (realLooper.getQueue()) {
shadowQueue.setUncaughtException(e);
- // release any ControlRunnables currently in queue to prevent deadlocks
- shadowQueue.drainQueue(
- input -> {
- if (input instanceof ControlRunnable) {
- ((ControlRunnable) input).runLatch.countDown();
- return true;
- }
- return false;
- });
+ drainQueueSafely(shadowQueue);
}
}
if (e instanceof ControlException) {
@@ -476,6 +491,18 @@ public final class ShadowPausedLooper extends ShadowLooper {
}
}
+ private static void drainQueueSafely(ShadowPausedMessageQueue shadowQueue) {
+ // release any ControlRunnables currently in queue to prevent deadlocks
+ shadowQueue.drainQueue(
+ input -> {
+ if (input instanceof ControlRunnable) {
+ ((ControlRunnable) input).runLatch.countDown();
+ return true;
+ }
+ return false;
+ });
+ }
+
/**
* If the given {@code lastMessageRead} is not null and the queue is now idle, get the idle
* handlers and run them. This synchronization mirrors what happens in the real message queue
@@ -618,8 +645,37 @@ public final class ShadowPausedLooper extends ShadowLooper {
}
}
- /** Executes the given runnable on the loopers thread, and waits for it to complete. */
- private void executeOnLooper(ControlRunnable runnable) {
+ private class PostAsyncAndIdleToRunnable extends ControlRunnable {
+ private final Runnable runnable;
+ private final Handler handler;
+
+ PostAsyncAndIdleToRunnable(Runnable runnable) {
+ this.runnable = runnable;
+ this.handler = createAsyncHandler(realLooper);
+ }
+
+ @Override
+ public void doRun() {
+ handler.postAtFrontOfQueue(runnable);
+ Message msg;
+ do {
+ msg = getNextExecutableMessage();
+ if (msg == null) {
+ throw new IllegalStateException("Runnable is not in the queue");
+ }
+ msg.getTarget().dispatchMessage(msg);
+
+ } while (msg.getCallback() != runnable);
+ }
+ }
+
+ /**
+ * Executes the given runnable on the loopers thread, and waits for it to complete.
+ *
+ * @throws IllegalStateException if Looper is quitting or has stopped due to uncaught exception
+ */
+ private void executeOnLooper(ControlRunnable runnable) throws IllegalStateException {
+ checkState(!shadowQueue().isQuitting(), "Looper is quitting");
if (Thread.currentThread() == realLooper.getThread()) {
if (runnable instanceof UnPauseRunnable) {
// Need to trigger the unpause action in PausedLooperExecutor
@@ -682,16 +738,27 @@ public final class ShadowPausedLooper extends ShadowLooper {
private class UnPauseRunnable extends ControlRunnable {
@Override
public void doRun() {
- setLooperExecutor(new HandlerExecutor(new Handler(realLooper)));
+ setLooperExecutor(new HandlerExecutor(realLooper));
isPaused = false;
}
}
+ static Handler createAsyncHandler(Looper looper) {
+ if (RuntimeEnvironment.getApiLevel() >= 28) {
+ // createAsync is only available in API 28+
+ return Handler.createAsync(looper);
+ } else {
+ return new Handler(looper, null, true);
+ }
+ }
+
private static class HandlerExecutor implements Executor {
private final Handler handler;
- private HandlerExecutor(Handler handler) {
- this.handler = handler;
+ private HandlerExecutor(Looper looper) {
+ // always post async messages so ControlRunnables get processed even if Looper is blocked on a
+ // sync barrier
+ this.handler = createAsyncHandler(looper);
}
@Override