/* * 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 android.testing; 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 sThreadLocal = (ThreadLocal) 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 sThreadLocal = (ThreadLocal) 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 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 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(); } try { mBase.evaluate(); } finally { 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); } }