blob: e8ceb4a9b02da69c23e54de290dfb6947fd05b83 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.os.test;
import static org.junit.Assert.assertTrue;
import android.os.Looper;
import android.os.Message;
import android.os.MessageQueue;
import android.os.SystemClock;
import android.util.Log;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* Creates a looper whose message queue can be manipulated
* This allows testing code that uses a looper to dispatch messages in a deterministic manner
* Creating a TestLooper will also install it as the looper for the current thread
*/
public class TestLooper {
protected final Looper mLooper;
private static final Constructor<Looper> LOOPER_CONSTRUCTOR;
private static final Field THREAD_LOCAL_LOOPER_FIELD;
private static final Field MESSAGE_QUEUE_MESSAGES_FIELD;
private static final Field MESSAGE_NEXT_FIELD;
private static final Field MESSAGE_WHEN_FIELD;
private static final Method MESSAGE_MARK_IN_USE_METHOD;
private static final String TAG = "TestLooper";
private AutoDispatchThread mAutoDispatchThread;
static {
try {
LOOPER_CONSTRUCTOR = Looper.class.getDeclaredConstructor(Boolean.TYPE);
LOOPER_CONSTRUCTOR.setAccessible(true);
THREAD_LOCAL_LOOPER_FIELD = Looper.class.getDeclaredField("sThreadLocal");
THREAD_LOCAL_LOOPER_FIELD.setAccessible(true);
MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages");
MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true);
MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next");
MESSAGE_NEXT_FIELD.setAccessible(true);
MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when");
MESSAGE_WHEN_FIELD.setAccessible(true);
MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse");
MESSAGE_MARK_IN_USE_METHOD.setAccessible(true);
} catch (NoSuchFieldException | NoSuchMethodException e) {
throw new RuntimeException("Failed to initialize TestLooper", e);
}
}
public TestLooper() {
try {
mLooper = LOOPER_CONSTRUCTOR.newInstance(false);
ThreadLocal<Looper> threadLocalLooper = (ThreadLocal<Looper>) THREAD_LOCAL_LOOPER_FIELD
.get(null);
threadLocalLooper.set(mLooper);
} catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
throw new RuntimeException("Reflection error constructing or accessing looper", e);
}
}
public Looper getLooper() {
return mLooper;
}
private Message getMessageLinkedList() {
try {
MessageQueue queue = mLooper.getQueue();
return (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(queue);
} catch (IllegalAccessException e) {
throw new RuntimeException("Access failed in TestLooper: get - MessageQueue.mMessages",
e);
}
}
public void moveTimeForward(long milliSeconds) {
try {
Message msg = getMessageLinkedList();
while (msg != null) {
long updatedWhen = msg.getWhen() - milliSeconds;
if (updatedWhen < 0) {
updatedWhen = 0;
}
MESSAGE_WHEN_FIELD.set(msg, updatedWhen);
msg = (Message) MESSAGE_NEXT_FIELD.get(msg);
}
} catch (IllegalAccessException e) {
throw new RuntimeException("Access failed in TestLooper: set - Message.when", e);
}
}
private Message messageQueueNext() {
try {
long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = getMessageLinkedList();
if (msg != null && msg.getTarget() == null) {
// Stalled by a barrier. Find the next asynchronous message in
// the queue.
do {
prevMsg = msg;
msg = (Message) MESSAGE_NEXT_FIELD.get(msg);
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now >= msg.getWhen()) {
// Got a message.
if (prevMsg != null) {
MESSAGE_NEXT_FIELD.set(prevMsg, MESSAGE_NEXT_FIELD.get(msg));
} else {
MESSAGE_QUEUE_MESSAGES_FIELD.set(mLooper.getQueue(),
MESSAGE_NEXT_FIELD.get(msg));
}
MESSAGE_NEXT_FIELD.set(msg, null);
MESSAGE_MARK_IN_USE_METHOD.invoke(msg);
return msg;
}
}
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException("Access failed in TestLooper", e);
}
return null;
}
/**
* @return true if there are pending messages in the message queue
*/
public synchronized boolean isIdle() {
Message messageList = getMessageLinkedList();
return messageList != null && SystemClock.uptimeMillis() >= messageList.getWhen();
}
/**
* @return the next message in the Looper's message queue or null if there is none
*/
public synchronized Message nextMessage() {
if (isIdle()) {
return messageQueueNext();
} else {
return null;
}
}
/**
* Dispatch the next message in the queue
* Asserts that there is a message in the queue
*/
public synchronized void dispatchNext() {
assertTrue(isIdle());
Message msg = messageQueueNext();
if (msg == null) {
return;
}
msg.getTarget().dispatchMessage(msg);
}
/**
* Dispatch all messages currently in the queue
* Will not fail if there are no messages pending
* @return the number of messages dispatched
*/
public synchronized int dispatchAll() {
int count = 0;
while (isIdle()) {
dispatchNext();
++count;
}
return count;
}
/**
* Thread used to dispatch messages when the main thread is blocked waiting for a response.
*/
private class AutoDispatchThread extends Thread {
private static final int MAX_LOOPS = 100;
private static final int LOOP_SLEEP_TIME_MS = 10;
private RuntimeException mAutoDispatchException = null;
/**
* Run method for the auto dispatch thread.
* The thread loops a maximum of MAX_LOOPS times with a 10ms sleep between loops.
* The thread continues looping and attempting to dispatch all messages until at
* least one message has been dispatched.
*/
@Override
public void run() {
int dispatchCount = 0;
for (int i = 0; i < MAX_LOOPS; i++) {
try {
dispatchCount = dispatchAll();
} catch (RuntimeException e) {
mAutoDispatchException = e;
}
Log.d(TAG, "dispatched " + dispatchCount + " messages");
if (dispatchCount > 0) {
return;
}
try {
Thread.sleep(LOOP_SLEEP_TIME_MS);
} catch (InterruptedException e) {
mAutoDispatchException = new IllegalStateException(
"stopAutoDispatch called before any messages were dispatched.");
return;
}
}
Log.e(TAG, "AutoDispatchThread did not dispatch any messages.");
mAutoDispatchException = new IllegalStateException(
"TestLooper did not dispatch any messages before exiting.");
}
/**
* Method allowing the TestLooper to pass any exceptions thrown by the thread to be passed
* to the main thread.
*
* @return RuntimeException Exception created by stopping without dispatching a message
*/
public RuntimeException getException() {
return mAutoDispatchException;
}
}
/**
* Create and start a new AutoDispatchThread if one is not already running.
*/
public void startAutoDispatch() {
if (mAutoDispatchThread != null) {
throw new IllegalStateException(
"startAutoDispatch called with the AutoDispatchThread already running.");
}
mAutoDispatchThread = new AutoDispatchThread();
mAutoDispatchThread.start();
}
/**
* If an AutoDispatchThread is currently running, stop and clean up.
*/
public void stopAutoDispatch() {
if (mAutoDispatchThread != null) {
if (mAutoDispatchThread.isAlive()) {
mAutoDispatchThread.interrupt();
}
try {
mAutoDispatchThread.join();
} catch (InterruptedException e) {
// Catch exception from join.
}
RuntimeException e = mAutoDispatchThread.getException();
mAutoDispatchThread = null;
if (e != null) {
throw e;
}
} else {
// stopAutoDispatch was called when startAutoDispatch has not created a new thread.
throw new IllegalStateException(
"stopAutoDispatch called without startAutoDispatch.");
}
}
}