
org.robolectric.shadows.ShadowPausedMessageQueue Maven / Gradle / Ivy
Show all versions of shadows-framework Show documentation
package org.robolectric.shadows;
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.RuntimeEnvironment.getApiLevel;
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;
import android.os.SystemClock;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.google.common.base.Predicate;
import java.time.Duration;
import java.util.ArrayList;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.RealObject;
import org.robolectric.res.android.NativeObjRegistry;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowMessage.MessageReflector;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.Scheduler;
import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;
import org.robolectric.versioning.AndroidVersions.V;
/**
* The shadow {@link} MessageQueue} for {@link LooperMode.Mode.PAUSED}
*
* This class should not be referenced directly. Use {@link ShadowMessageQueue} instead.
*/
@SuppressWarnings("SynchronizeOnNonFinalField")
@Implements(value = MessageQueue.class, isInAndroidSdk = false)
public class ShadowPausedMessageQueue extends ShadowMessageQueue {
@RealObject private MessageQueue realQueue;
// just use this class as the native object
private static NativeObjRegistry nativeQueueRegistry =
new NativeObjRegistry(ShadowPausedMessageQueue.class);
private boolean isPolling = false;
private ShadowPausedSystemClock.Listener clockListener;
private Exception uncaughtException = null;
// shadow constructor instead of nativeInit because nativeInit signature has changed across SDK
// versions
@Implementation
protected void __constructor__(boolean quitAllowed) {
invokeConstructor(MessageQueue.class, realQueue, from(boolean.class, quitAllowed));
long ptr = nativeQueueRegistry.register(this);
reflector(MessageQueueReflector.class, realQueue).setPtr(ptr);
clockListener =
() -> {
synchronized (realQueue) {
// only wake up the Looper thread if queue is non empty to reduce contention if many
// Looper threads are active
if (getMessages() != null) {
nativeWake(ptr);
}
}
};
ShadowPausedSystemClock.addStaticListener(clockListener);
}
@Implementation
protected static void nativeDestroy(long ptr) {
ShadowPausedMessageQueue q = nativeQueueRegistry.unregister(ptr);
ShadowPausedSystemClock.removeListener(q.clockListener);
}
@Implementation(maxSdk = LOLLIPOP_MR1, methodName = "nativePollOnce")
protected static void nativePollOncePreM(long ptr, int timeoutMillis) {
nativeQueueRegistry.getNativeObject(ptr).nativePollOnce(ptr, timeoutMillis);
}
@Implementation(minSdk = M)
protected void nativePollOnce(long ptr, int timeoutMillis) {
if (timeoutMillis == 0) {
return;
}
synchronized (realQueue) {
// only block if queue is empty
// ignore timeout since clock is not advancing. ClockListener will notify when clock advances
while (isIdle() && !isQuitting()) {
isPolling = true;
try {
realQueue.wait();
} catch (InterruptedException e) {
// ignore
}
}
isPolling = false;
}
}
/**
* 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
protected static void nativeWake(long ptr) {
MessageQueue realQueue = nativeQueueRegistry.getNativeObject(ptr).realQueue;
synchronized (realQueue) {
realQueue.notifyAll();
}
}
@Implementation(minSdk = M)
protected static boolean nativeIsPolling(long ptr) {
return nativeQueueRegistry.getNativeObject(ptr).isPolling;
}
/** Exposes the API23+_isIdle method to older platforms */
@Implementation(minSdk = 23)
public boolean isIdle() {
synchronized (realQueue) {
Message msg = peekNextExecutableMessage();
if (msg == null) {
return true;
}
long now = SystemClock.uptimeMillis();
long when = shadowOfMsg(msg).getWhen();
return now < when;
}
}
Message peekNextExecutableMessage() {
MessageQueueReflector internalQueue = reflector(MessageQueueReflector.class, realQueue);
Message msg = internalQueue.getMessages();
if (msg != null && shadowOfMsg(msg).getTarget() == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
msg = shadowOfMsg(msg).internalGetNext();
} while (msg != null && !msg.isAsynchronous());
}
return msg;
}
Message getNext() {
return reflector(MessageQueueReflector.class, realQueue).next();
}
boolean isQuitAllowed() {
return reflector(MessageQueueReflector.class, realQueue).getQuitAllowed();
}
@VisibleForTesting
void doEnqueueMessage(Message msg, long when) {
enqueueMessage(msg, when);
}
@Implementation
protected boolean enqueueMessage(Message msg, long when) {
synchronized (realQueue) {
if (uncaughtException != null) {
// looper thread has died
IllegalStateException e =
new IllegalStateException(
msg.getTarget()
+ " sending message to a Looper thread that has died due to an uncaught"
+ " exception",
uncaughtException);
Log.w("ShadowPausedMessageQueue", e);
msg.recycle();
throw e;
}
return reflector(MessageQueueReflector.class, realQueue).enqueueMessage(msg, when);
}
}
Message getMessages() {
return reflector(MessageQueueReflector.class, realQueue).getMessages();
}
@Implementation(minSdk = M)
protected boolean isPolling() {
synchronized (realQueue) {
return isPolling;
}
}
void quit() {
quit(true);
}
@Implementation
protected void quit(boolean allowed) {
reflector(MessageQueueReflector.class, realQueue).quit(allowed);
ShadowPausedSystemClock.removeListener(clockListener);
}
boolean isQuitting() {
return reflector(MessageQueueReflector.class, realQueue).getQuitting();
}
Duration getNextScheduledTaskTime() {
Message next = peekNextExecutableMessage();
if (next == null) {
return Duration.ZERO;
}
return Duration.ofMillis(convertWhenToScheduledTime(shadowOfMsg(next).getWhen()));
}
Duration getLastScheduledTaskTime() {
long when = 0;
synchronized (realQueue) {
Message next = getMessages();
if (next == null) {
return Duration.ZERO;
}
while (next != null) {
if (next.getTarget() != null) {
when = shadowOfMsg(next).getWhen();
}
next = shadowOfMsg(next).internalGetNext();
}
}
return Duration.ofMillis(convertWhenToScheduledTime(when));
}
private static long convertWhenToScheduledTime(long when) {
// in some situations, when can be 0 or less than uptimeMillis. Always floor it to at least
// convertWhenToUptime
if (when < SystemClock.uptimeMillis()) {
when = SystemClock.uptimeMillis();
}
return when;
}
/**
* Internal method to get the number of entries in the MessageQueue.
*
*
Do not use, will likely be removed in a future release.
*/
public int internalGetSize() {
int count = 0;
synchronized (realQueue) {
Message next = getMessages();
while (next != null) {
if (next.getTarget() != null) {
count++;
}
next = shadowOfMsg(next).internalGetNext();
}
}
return count;
}
/**
* 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.
*
*
This is a copy of the real MessageQueue.next implementation with the 'when' handling logic
* omitted.
*/
Message getNextIgnoringWhen() {
MessageQueueReflector queueReflector = reflector(MessageQueueReflector.class, realQueue);
synchronized (realQueue) {
Message prevMsg = null;
Message msg = getMessages();
// Head is blocked on synchronization barrier, find next asynchronous message.
if (msg != null && msg.getTarget() == null) {
do {
prevMsg = msg;
msg = shadowOfMsg(msg).internalGetNext();
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
Message nextMsg = reflector(MessageReflector.class, msg).getNext();
if (prevMsg != null) {
reflector(MessageReflector.class, prevMsg).setNext(nextMsg);
if (reflector(MessageReflector.class, prevMsg).getNext() == null
&& getApiLevel() >= V.SDK_INT) {
queueReflector.setLast(prevMsg);
}
} else {
queueReflector.setMessages(nextMsg);
if (nextMsg == null && getApiLevel() >= V.SDK_INT) {
queueReflector.setLast(null);
}
}
if (msg.isAsynchronous() && getApiLevel() >= V.SDK_INT) {
queueReflector.setAsyncMessageCount(queueReflector.getAsyncMessageCount() - 1);
}
}
return msg;
}
}
// TODO: reconsider exposing this as a public API. Only ShadowPausedLooper needs to access this,
// so it should be package private
@Override
public void reset() {
MessageQueueReflector msgQueue = reflector(MessageQueueReflector.class, realQueue);
synchronized (realQueue) {
msgQueue.setMessages(null);
msgQueue.setIdleHandlers(new ArrayList<>());
msgQueue.setNextBarrierToken(0);
if (getApiLevel() >= V.SDK_INT) {
msgQueue.setLast(null);
msgQueue.setAsyncMessageCount(0);
}
}
setUncaughtException(null);
}
private static ShadowPausedMessage shadowOfMsg(Message head) {
return Shadow.extract(head);
}
@Override
public Scheduler getScheduler() {
throw new UnsupportedOperationException("Not supported in PAUSED LooperMode.");
}
@Override
public void setScheduler(Scheduler scheduler) {
throw new UnsupportedOperationException("Not supported in PAUSED LooperMode.");
}
// intentionally do not support direct access to MessageQueue internals
@Override
public Message getHead() {
throw new UnsupportedOperationException("Not supported in PAUSED LooperMode.");
}
@Override
public void setHead(Message msg) {
throw new UnsupportedOperationException("Not supported in PAUSED LooperMode.");
}
/**
* Retrieves a copy of the current list of idle handlers. Idle handlers are read with
* synchronization on the real queue.
*/
ArrayList getIdleHandlersCopy() {
synchronized (realQueue) {
return new ArrayList<>(reflector(MessageQueueReflector.class, realQueue).getIdleHandlers());
}
}
/**
* Called when an uncaught exception occurred in this message queue's Looper thread.
*
* In real android, by default an exception handler is installed which kills the entire process
* when an uncaught exception occurs. We don't want to do this in robolectric to isolate tests, so
* instead an uncaught exception puts the message queue into an error state, where any future
* interaction will rethrow the exception.
*/
void setUncaughtException(Exception e) {
synchronized (realQueue) {
this.uncaughtException = e;
}
}
boolean hasUncaughtException() {
synchronized (realQueue) {
return uncaughtException != null;
}
}
void checkQueueState() {
synchronized (realQueue) {
if (uncaughtException != null) {
throw new IllegalStateException(
"Looper thread that has died due to an uncaught exception", uncaughtException);
}
}
}
/**
* Remove all messages from queue
*
* @param msgProcessor a callback to apply to each mesg
*/
void drainQueue(Predicate msgProcessor) {
synchronized (realQueue) {
Message msg = getMessages();
while (msg != null) {
boolean unused = msgProcessor.apply(msg.getCallback());
Message next = shadowOfMsg(msg).internalGetNext();
shadowOfMsg(msg).recycleUnchecked();
msg = next;
}
reflector(MessageQueueReflector.class, realQueue).setMessages(null);
if (getApiLevel() >= V.SDK_INT) {
reflector(MessageQueueReflector.class, realQueue).setLast(null);
reflector(MessageQueueReflector.class, realQueue).setAsyncMessageCount(0);
}
}
}
/** Accessor interface for {@link MessageQueue}'s internals. */
@ForType(MessageQueue.class)
private interface MessageQueueReflector {
@Direct
boolean enqueueMessage(Message msg, long when);
Message next();
@Accessor("mMessages")
void setMessages(Message msg);
@Accessor("mMessages")
Message getMessages();
@Accessor("mIdleHandlers")
void setIdleHandlers(ArrayList list);
@Accessor("mIdleHandlers")
ArrayList getIdleHandlers();
@Accessor("mNextBarrierToken")
void setNextBarrierToken(int token);
@Accessor("mQuitAllowed")
boolean getQuitAllowed();
@Accessor("mPtr")
void setPtr(long ptr);
@Direct
void quit(boolean b);
@Accessor("mQuitting")
boolean getQuitting();
// start for android V
@Accessor("mLast")
void setLast(Message msg);
@Accessor("mAsyncMessageCount")
int getAsyncMessageCount();
@Accessor("mAsyncMessageCount")
void setAsyncMessageCount(int asyncMessageCount);
// end android V
}
}