blob: 8a33cf918646d45374b1b9782a1189dedd4823b0 [file] [log] [blame]
Jason Monke7507482017-02-02 13:00:05 -05001/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5 * except in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the
10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11 * KIND, either express or implied. See the License for the specific language governing
12 * permissions and limitations under the License.
13 */
14
Jason Monk340b0e52017-03-08 14:57:56 -050015package android.testing;
Jason Monke7507482017-02-02 13:00:05 -050016
17import android.os.Handler;
18import android.os.Looper;
19import android.os.Message;
20import android.os.MessageQueue;
21import android.util.ArrayMap;
22
Jason Monkfd8f6152017-03-24 12:44:34 +000023import org.junit.runners.model.Statement;
Jason Monke7507482017-02-02 13:00:05 -050024
25import java.lang.annotation.ElementType;
26import java.lang.annotation.Retention;
27import java.lang.annotation.RetentionPolicy;
28import java.lang.annotation.Target;
Jason Monkfd8f6152017-03-24 12:44:34 +000029import java.lang.reflect.Constructor;
Jason Monke7507482017-02-02 13:00:05 -050030import java.lang.reflect.Field;
Jason Monkfd8f6152017-03-24 12:44:34 +000031import java.lang.reflect.Method;
Jason Monke7507482017-02-02 13:00:05 -050032import java.util.Map;
33
34/**
35 * Creates a looper on the current thread with control over if/when messages are
36 * executed. Warning: This class works through some reflection and may break/need
37 * to be updated from time to time.
38 */
39public class TestableLooper {
40
Jason Monkfd8f6152017-03-24 12:44:34 +000041 private final Method mNext;
42 private final Method mRecycleUnchecked;
43
Jason Monke7507482017-02-02 13:00:05 -050044 private Looper mLooper;
45 private MessageQueue mQueue;
46 private boolean mMain;
47 private Object mOriginalMain;
48 private MessageHandler mMessageHandler;
49
Jason Monkfd8f6152017-03-24 12:44:34 +000050 private int mParsedCount;
Jason Monke7507482017-02-02 13:00:05 -050051 private Handler mHandler;
52 private Message mEmptyMessage;
53
Jason Monkfd8f6152017-03-24 12:44:34 +000054 public TestableLooper() throws Exception {
55 this(true);
Jason Monke7507482017-02-02 13:00:05 -050056 }
57
Jason Monkfd8f6152017-03-24 12:44:34 +000058 public TestableLooper(boolean setMyLooper) throws Exception {
59 setupQueue(setMyLooper);
60 mNext = mQueue.getClass().getDeclaredMethod("next");
61 mNext.setAccessible(true);
62 mRecycleUnchecked = Message.class.getDeclaredMethod("recycleUnchecked");
63 mRecycleUnchecked.setAccessible(true);
Jason Monke7507482017-02-02 13:00:05 -050064 }
65
66 public Looper getLooper() {
67 return mLooper;
68 }
69
Jason Monkfd8f6152017-03-24 12:44:34 +000070 private void clearLooper() throws NoSuchFieldException, IllegalAccessException {
71 Field field = Looper.class.getDeclaredField("sThreadLocal");
72 field.setAccessible(true);
73 ThreadLocal<Looper> sThreadLocal = (ThreadLocal<Looper>) field.get(null);
74 sThreadLocal.set(null);
75 }
76
77 private boolean setForCurrentThread() throws NoSuchFieldException, IllegalAccessException {
78 if (Looper.myLooper() != mLooper) {
79 Field field = Looper.class.getDeclaredField("sThreadLocal");
80 field.setAccessible(true);
81 ThreadLocal<Looper> sThreadLocal = (ThreadLocal<Looper>) field.get(null);
82 sThreadLocal.set(mLooper);
83 return true;
84 }
85 return false;
86 }
87
88 private void setupQueue(boolean setMyLooper) throws Exception {
89 if (setMyLooper) {
90 clearLooper();
91 Looper.prepare();
92 mLooper = Looper.myLooper();
93 } else {
94 Constructor<Looper> constructor = Looper.class.getDeclaredConstructor(
95 boolean.class);
96 constructor.setAccessible(true);
97 mLooper = constructor.newInstance(true);
98 }
99
Jason Monke7507482017-02-02 13:00:05 -0500100 mQueue = mLooper.getQueue();
101 mHandler = new Handler(mLooper);
102 }
103
104 public void setAsMainLooper() throws NoSuchFieldException, IllegalAccessException {
105 mMain = true;
106 setAsMainInt();
107 }
108
109 private void setAsMainInt() throws NoSuchFieldException, IllegalAccessException {
110 Field field = mLooper.getClass().getDeclaredField("sMainLooper");
111 field.setAccessible(true);
112 if (mOriginalMain == null) {
113 mOriginalMain = field.get(null);
114 }
115 field.set(null, mLooper);
116 }
117
118 /**
119 * Must be called if setAsMainLooper is called to restore the main looper when the
120 * test is complete, otherwise the main looper will not be available for any subsequent
121 * tests.
122 */
123 public void destroy() throws NoSuchFieldException, IllegalAccessException {
Jason Monkfd8f6152017-03-24 12:44:34 +0000124 if (Looper.myLooper() == mLooper) {
125 clearLooper();
126 }
Jason Monke7507482017-02-02 13:00:05 -0500127 if (mMain && mOriginalMain != null) {
128 Field field = mLooper.getClass().getDeclaredField("sMainLooper");
129 field.setAccessible(true);
130 field.set(null, mOriginalMain);
131 mOriginalMain = null;
132 }
133 }
134
135 public void setMessageHandler(MessageHandler handler) {
136 mMessageHandler = handler;
137 }
138
139 /**
140 * Parse num messages from the message queue.
141 *
142 * @param num Number of messages to parse
143 */
144 public int processMessages(int num) {
145 for (int i = 0; i < num; i++) {
146 if (!parseMessageInt()) {
147 return i + 1;
148 }
149 }
150 return num;
151 }
152
153 public void processAllMessages() {
154 while (processQueuedMessages() != 0) ;
155 }
156
157 private int processQueuedMessages() {
158 int count = 0;
159 mEmptyMessage = mHandler.obtainMessage(1);
160 mHandler.sendMessageDelayed(mEmptyMessage, 1);
161 while (parseMessageInt()) count++;
162 return count;
163 }
164
165 private boolean parseMessageInt() {
166 try {
Jason Monkfd8f6152017-03-24 12:44:34 +0000167 Message result = (Message) mNext.invoke(mQueue);
Jason Monke7507482017-02-02 13:00:05 -0500168 if (result != null) {
169 // This is a break message.
170 if (result == mEmptyMessage) {
Jason Monkfd8f6152017-03-24 12:44:34 +0000171 mRecycleUnchecked.invoke(result);
Jason Monke7507482017-02-02 13:00:05 -0500172 return false;
173 }
174
175 if (mMessageHandler != null) {
176 if (mMessageHandler.onMessageHandled(result)) {
177 result.getTarget().dispatchMessage(result);
Jason Monkfd8f6152017-03-24 12:44:34 +0000178 mRecycleUnchecked.invoke(result);
Jason Monke7507482017-02-02 13:00:05 -0500179 } else {
Jason Monkfd8f6152017-03-24 12:44:34 +0000180 mRecycleUnchecked.invoke(result);
Jason Monke7507482017-02-02 13:00:05 -0500181 // Message handler indicated it doesn't want us to continue.
182 return false;
183 }
184 } else {
185 result.getTarget().dispatchMessage(result);
Jason Monkfd8f6152017-03-24 12:44:34 +0000186 mRecycleUnchecked.invoke(result);
Jason Monke7507482017-02-02 13:00:05 -0500187 }
188 } else {
189 // No messages, don't continue parsing
190 return false;
191 }
192 } catch (Exception e) {
193 throw new RuntimeException(e);
194 }
195 return true;
196 }
197
198 /**
199 * Runs an executable with myLooper set and processes all messages added.
200 */
201 public void runWithLooper(RunnableWithException runnable) throws Exception {
Jason Monkfd8f6152017-03-24 12:44:34 +0000202 boolean set = setForCurrentThread();
203 runnable.run();
Jason Monke7507482017-02-02 13:00:05 -0500204 processAllMessages();
Jason Monkfd8f6152017-03-24 12:44:34 +0000205 if (set) clearLooper();
Jason Monke7507482017-02-02 13:00:05 -0500206 }
207
208 public interface RunnableWithException {
209 void run() throws Exception;
210 }
211
212 @Retention(RetentionPolicy.RUNTIME)
213 @Target({ElementType.METHOD, ElementType.TYPE})
214 public @interface RunWithLooper {
215 boolean setAsMainLooper() default false;
216 }
217
218 private static final Map<Object, TestableLooper> sLoopers = new ArrayMap<>();
219
220 public static TestableLooper get(Object test) {
221 return sLoopers.get(test);
222 }
223
Jason Monkfd8f6152017-03-24 12:44:34 +0000224 public static class LooperStatement extends Statement {
225 private final boolean mSetAsMain;
226 private final Statement mBase;
227 private final TestableLooper mLooper;
Jason Monke7507482017-02-02 13:00:05 -0500228
Jason Monkfd8f6152017-03-24 12:44:34 +0000229 public LooperStatement(Statement base, boolean setAsMain, Object test) {
230 mBase = base;
Jason Monke7507482017-02-02 13:00:05 -0500231 try {
Jason Monkfd8f6152017-03-24 12:44:34 +0000232 mLooper = new TestableLooper(false);
233 sLoopers.put(test, mLooper);
234 mSetAsMain = setAsMain;
Jason Monke7507482017-02-02 13:00:05 -0500235 } catch (Exception e) {
236 throw new RuntimeException(e);
237 }
238 }
239
240 @Override
Jason Monkfd8f6152017-03-24 12:44:34 +0000241 public void evaluate() throws Throwable {
242 mLooper.setForCurrentThread();
243 if (mSetAsMain) {
244 mLooper.setAsMainLooper();
Jason Monkf715f412017-03-14 17:16:56 -0400245 }
Jason Monkfd8f6152017-03-24 12:44:34 +0000246
Jason Monkf715f412017-03-14 17:16:56 -0400247 try {
Jason Monkfd8f6152017-03-24 12:44:34 +0000248 mBase.evaluate();
Jason Monkf715f412017-03-14 17:16:56 -0400249 } finally {
Jason Monkfd8f6152017-03-24 12:44:34 +0000250 mLooper.destroy();
Jason Monk9ec0f1e2017-02-23 11:33:54 -0500251 }
Jason Monke7507482017-02-02 13:00:05 -0500252 }
253 }
254
255 public interface MessageHandler {
256 /**
257 * Return true to have the message executed and delivered to target.
258 * Return false to not execute the message and stop executing messages.
259 */
260 boolean onMessageHandled(Message m);
261 }
262}