blob: d27597331aa206599f3e7f19fb259401532f3d09 [file] [log] [blame]
/*
* Copyright (C) 2017 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 com.android.systemui.utils;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.MessageQueue;
import android.util.ArrayMap;
import org.junit.runners.model.Statement;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
/**
* Creates a looper on the current thread with control over if/when messages are
* executed. Warning: This class works through some reflection and may break/need
* to be updated from time to time.
*/
public class TestableLooper {
private final Method mNext;
private final Method mRecycleUnchecked;
private Looper mLooper;
private MessageQueue mQueue;
private boolean mMain;
private Object mOriginalMain;
private MessageHandler mMessageHandler;
private int mParsedCount;
private Handler mHandler;
private Message mEmptyMessage;
public TestableLooper() throws Exception {
this(true);
}
public TestableLooper(boolean setMyLooper) throws Exception {
setupQueue(setMyLooper);
mNext = mQueue.getClass().getDeclaredMethod("next");
mNext.setAccessible(true);
mRecycleUnchecked = Message.class.getDeclaredMethod("recycleUnchecked");
mRecycleUnchecked.setAccessible(true);
}
public Looper getLooper() {
return mLooper;
}
private void clearLooper() throws NoSuchFieldException, IllegalAccessException {
Field field = Looper.class.getDeclaredField("sThreadLocal");
field.setAccessible(true);
ThreadLocal<Looper> sThreadLocal = (ThreadLocal<Looper>) field.get(null);
sThreadLocal.set(null);
}
private boolean setForCurrentThread() throws NoSuchFieldException, IllegalAccessException {
if (Looper.myLooper() != mLooper) {
Field field = Looper.class.getDeclaredField("sThreadLocal");
field.setAccessible(true);
ThreadLocal<Looper> sThreadLocal = (ThreadLocal<Looper>) field.get(null);
sThreadLocal.set(mLooper);
return true;
}
return false;
}
private void setupQueue(boolean setMyLooper) throws Exception {
if (setMyLooper) {
clearLooper();
Looper.prepare();
mLooper = Looper.myLooper();
} else {
Constructor<Looper> constructor = Looper.class.getDeclaredConstructor(
boolean.class);
constructor.setAccessible(true);
mLooper = constructor.newInstance(true);
}
mQueue = mLooper.getQueue();
mHandler = new Handler(mLooper);
}
public void setAsMainLooper() throws NoSuchFieldException, IllegalAccessException {
mMain = true;
setAsMainInt();
}
private void setAsMainInt() throws NoSuchFieldException, IllegalAccessException {
Field field = mLooper.getClass().getDeclaredField("sMainLooper");
field.setAccessible(true);
if (mOriginalMain == null) {
mOriginalMain = field.get(null);
}
field.set(null, mLooper);
}
/**
* Must be called if setAsMainLooper is called to restore the main looper when the
* test is complete, otherwise the main looper will not be available for any subsequent
* tests.
*/
public void destroy() throws NoSuchFieldException, IllegalAccessException {
if (Looper.myLooper() == mLooper) {
clearLooper();
}
if (mMain && mOriginalMain != null) {
Field field = mLooper.getClass().getDeclaredField("sMainLooper");
field.setAccessible(true);
field.set(null, mOriginalMain);
mOriginalMain = null;
}
}
public void setMessageHandler(MessageHandler handler) {
mMessageHandler = handler;
}
/**
* Parse num messages from the message queue.
*
* @param num Number of messages to parse
*/
public int processMessages(int num) {
for (int i = 0; i < num; i++) {
if (!parseMessageInt()) {
return i + 1;
}
}
return num;
}
public void processAllMessages() {
while (processQueuedMessages() != 0) ;
}
private int processQueuedMessages() {
int count = 0;
mEmptyMessage = mHandler.obtainMessage(1);
mHandler.sendMessageDelayed(mEmptyMessage, 1);
while (parseMessageInt()) count++;
return count;
}
private boolean parseMessageInt() {
try {
Message result = (Message) mNext.invoke(mQueue);
if (result != null) {
// This is a break message.
if (result == mEmptyMessage) {
mRecycleUnchecked.invoke(result);
return false;
}
if (mMessageHandler != null) {
if (mMessageHandler.onMessageHandled(result)) {
result.getTarget().dispatchMessage(result);
mRecycleUnchecked.invoke(result);
} else {
mRecycleUnchecked.invoke(result);
// Message handler indicated it doesn't want us to continue.
return false;
}
} else {
result.getTarget().dispatchMessage(result);
mRecycleUnchecked.invoke(result);
}
} else {
// No messages, don't continue parsing
return false;
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return true;
}
/**
* Runs an executable with myLooper set and processes all messages added.
*/
public void runWithLooper(RunnableWithException runnable) throws Exception {
boolean set = setForCurrentThread();
runnable.run();
processAllMessages();
if (set) clearLooper();
}
public interface RunnableWithException {
void run() throws Exception;
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RunWithLooper {
boolean setAsMainLooper() default false;
}
private static final Map<Object, TestableLooper> sLoopers = new ArrayMap<>();
public static TestableLooper get(Object test) {
return sLoopers.get(test);
}
public static class LooperStatement extends Statement {
private final boolean mSetAsMain;
private final Statement mBase;
private final TestableLooper mLooper;
public LooperStatement(Statement base, boolean setAsMain, Object test) {
mBase = base;
try {
mLooper = new TestableLooper(false);
sLoopers.put(test, mLooper);
mSetAsMain = setAsMain;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void evaluate() throws Throwable {
mLooper.setForCurrentThread();
if (mSetAsMain) {
mLooper.setAsMainLooper();
}
mBase.evaluate();
mLooper.destroy();
}
}
public interface MessageHandler {
/**
* Return true to have the message executed and delivered to target.
* Return false to not execute the message and stop executing messages.
*/
boolean onMessageHandled(Message m);
}
}