blob: ebe9b5706bf889db3c0560f67e9d9f3d00d2f7e1 [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;
Jason Monk745d0a82017-04-17 11:34:22 -040018import android.os.HandlerThread;
Jason Monke7507482017-02-02 13:00:05 -050019import android.os.Looper;
20import android.os.Message;
21import android.os.MessageQueue;
Jason Monk745d0a82017-04-17 11:34:22 -040022import android.os.TestLooperManager;
Jason Monke7507482017-02-02 13:00:05 -050023import android.util.ArrayMap;
24
Brett Chabot84151d92019-02-27 15:37:59 -080025import androidx.test.InstrumentationRegistry;
26
Jason Monk745d0a82017-04-17 11:34:22 -040027import org.junit.runners.model.FrameworkMethod;
Jason Monke7507482017-02-02 13:00:05 -050028
29import java.lang.annotation.ElementType;
30import java.lang.annotation.Retention;
31import java.lang.annotation.RetentionPolicy;
32import java.lang.annotation.Target;
Jason Monke7507482017-02-02 13:00:05 -050033import java.util.Map;
34
35/**
Jason Monk0c408002017-05-03 15:43:52 -040036 * This is a wrapper around {@link TestLooperManager} to make it easier to manage
37 * and provide an easy annotation for use with tests.
38 *
39 * @see TestableLooperTest TestableLooperTest for examples.
Jason Monke7507482017-02-02 13:00:05 -050040 */
41public class TestableLooper {
42
Jason Monk759e9122018-07-20 14:52:22 -040043 /**
44 * Whether to hold onto the main thread through all tests in an attempt to
45 * catch crashes.
46 */
47 public static final boolean HOLD_MAIN_THREAD = false;
48
Jason Monke7507482017-02-02 13:00:05 -050049 private Looper mLooper;
50 private MessageQueue mQueue;
Jason Monke7507482017-02-02 13:00:05 -050051 private MessageHandler mMessageHandler;
52
Jason Monke7507482017-02-02 13:00:05 -050053 private Handler mHandler;
Jason Monk745d0a82017-04-17 11:34:22 -040054 private Runnable mEmptyMessage;
55 private TestLooperManager mQueueWrapper;
Jason Monke7507482017-02-02 13:00:05 -050056
Jason Monk745d0a82017-04-17 11:34:22 -040057 public TestableLooper(Looper l) throws Exception {
Jason Monkc429f692017-06-27 13:13:49 -040058 this(acquireLooperManager(l), l);
Jason Monke7507482017-02-02 13:00:05 -050059 }
60
Jason Monk1e352f42018-05-16 10:15:33 -040061 private TestableLooper(TestLooperManager wrapper, Looper l) {
Jason Monk745d0a82017-04-17 11:34:22 -040062 mQueueWrapper = wrapper;
63 setupQueue(l);
64 }
65
Jason Monk1e352f42018-05-16 10:15:33 -040066 private TestableLooper(Looper looper, boolean b) {
Jason Monk745d0a82017-04-17 11:34:22 -040067 setupQueue(looper);
Jason Monke7507482017-02-02 13:00:05 -050068 }
69
70 public Looper getLooper() {
71 return mLooper;
72 }
73
Jason Monk1e352f42018-05-16 10:15:33 -040074 private void setupQueue(Looper l) {
Jason Monk745d0a82017-04-17 11:34:22 -040075 mLooper = l;
Jason Monke7507482017-02-02 13:00:05 -050076 mQueue = mLooper.getQueue();
77 mHandler = new Handler(mLooper);
78 }
79
Jason Monke7507482017-02-02 13:00:05 -050080 /**
Jason Monk0c408002017-05-03 15:43:52 -040081 * Must be called to release the looper when the test is complete, otherwise
82 * the looper will not be available for any subsequent tests. This is
83 * automatically handled for tests using {@link RunWithLooper}.
Jason Monke7507482017-02-02 13:00:05 -050084 */
Jason Monk1e352f42018-05-16 10:15:33 -040085 public void destroy() {
Jason Monk745d0a82017-04-17 11:34:22 -040086 mQueueWrapper.release();
Jason Monk759e9122018-07-20 14:52:22 -040087 if (HOLD_MAIN_THREAD && mLooper == Looper.getMainLooper()) {
Jason Monkc429f692017-06-27 13:13:49 -040088 TestableInstrumentation.releaseMain();
89 }
Jason Monke7507482017-02-02 13:00:05 -050090 }
91
Jason Monk0c408002017-05-03 15:43:52 -040092 /**
93 * Sets a callback for all messages processed on this TestableLooper.
94 *
95 * @see {@link MessageHandler}
96 */
Jason Monke7507482017-02-02 13:00:05 -050097 public void setMessageHandler(MessageHandler handler) {
98 mMessageHandler = handler;
99 }
100
101 /**
102 * Parse num messages from the message queue.
103 *
104 * @param num Number of messages to parse
105 */
106 public int processMessages(int num) {
107 for (int i = 0; i < num; i++) {
108 if (!parseMessageInt()) {
109 return i + 1;
110 }
111 }
112 return num;
113 }
114
Jason Monk0c408002017-05-03 15:43:52 -0400115 /**
116 * Process messages in the queue until no more are found.
117 */
Jason Monke7507482017-02-02 13:00:05 -0500118 public void processAllMessages() {
119 while (processQueuedMessages() != 0) ;
120 }
121
122 private int processQueuedMessages() {
123 int count = 0;
Jason Monk745d0a82017-04-17 11:34:22 -0400124 mEmptyMessage = () -> { };
125 mHandler.post(mEmptyMessage);
126 waitForMessage(mQueueWrapper, mHandler, mEmptyMessage);
Jason Monke7507482017-02-02 13:00:05 -0500127 while (parseMessageInt()) count++;
128 return count;
129 }
130
131 private boolean parseMessageInt() {
132 try {
Jason Monk745d0a82017-04-17 11:34:22 -0400133 Message result = mQueueWrapper.next();
Jason Monke7507482017-02-02 13:00:05 -0500134 if (result != null) {
135 // This is a break message.
Jason Monk745d0a82017-04-17 11:34:22 -0400136 if (result.getCallback() == mEmptyMessage) {
137 mQueueWrapper.recycle(result);
Jason Monke7507482017-02-02 13:00:05 -0500138 return false;
139 }
140
141 if (mMessageHandler != null) {
142 if (mMessageHandler.onMessageHandled(result)) {
Jason Monk1e352f42018-05-16 10:15:33 -0400143 mQueueWrapper.execute(result);
Jason Monk745d0a82017-04-17 11:34:22 -0400144 mQueueWrapper.recycle(result);
Jason Monke7507482017-02-02 13:00:05 -0500145 } else {
Jason Monk745d0a82017-04-17 11:34:22 -0400146 mQueueWrapper.recycle(result);
Jason Monke7507482017-02-02 13:00:05 -0500147 // Message handler indicated it doesn't want us to continue.
148 return false;
149 }
150 } else {
Jason Monk1e352f42018-05-16 10:15:33 -0400151 mQueueWrapper.execute(result);
Jason Monk745d0a82017-04-17 11:34:22 -0400152 mQueueWrapper.recycle(result);
Jason Monke7507482017-02-02 13:00:05 -0500153 }
154 } else {
155 // No messages, don't continue parsing
156 return false;
157 }
158 } catch (Exception e) {
159 throw new RuntimeException(e);
160 }
161 return true;
162 }
163
164 /**
165 * Runs an executable with myLooper set and processes all messages added.
166 */
167 public void runWithLooper(RunnableWithException runnable) throws Exception {
Jason Monk745d0a82017-04-17 11:34:22 -0400168 new Handler(getLooper()).post(() -> {
169 try {
170 runnable.run();
171 } catch (Exception e) {
172 throw new RuntimeException(e);
173 }
174 });
Jason Monke7507482017-02-02 13:00:05 -0500175 processAllMessages();
Jason Monke7507482017-02-02 13:00:05 -0500176 }
177
178 public interface RunnableWithException {
179 void run() throws Exception;
180 }
181
Jason Monk0c408002017-05-03 15:43:52 -0400182 /**
183 * Annotation that tells the {@link AndroidTestingRunner} to create a TestableLooper and
184 * run this test/class on that thread. The {@link TestableLooper} can be acquired using
185 * {@link #get(Object)}.
186 */
Jason Monke7507482017-02-02 13:00:05 -0500187 @Retention(RetentionPolicy.RUNTIME)
188 @Target({ElementType.METHOD, ElementType.TYPE})
189 public @interface RunWithLooper {
190 boolean setAsMainLooper() default false;
191 }
192
Jason Monk745d0a82017-04-17 11:34:22 -0400193 private static void waitForMessage(TestLooperManager queueWrapper, Handler handler,
194 Runnable execute) {
195 for (int i = 0; i < 10; i++) {
196 if (!queueWrapper.hasMessages(handler, null, execute)) {
197 try {
198 Thread.sleep(1);
199 } catch (InterruptedException e) {
200 }
201 }
202 }
203 if (!queueWrapper.hasMessages(handler, null, execute)) {
204 throw new RuntimeException("Message didn't queue...");
205 }
206 }
207
Jason Monkc429f692017-06-27 13:13:49 -0400208 private static TestLooperManager acquireLooperManager(Looper l) {
Jason Monk759e9122018-07-20 14:52:22 -0400209 if (HOLD_MAIN_THREAD && l == Looper.getMainLooper()) {
Jason Monkc429f692017-06-27 13:13:49 -0400210 TestableInstrumentation.acquireMain();
211 }
212 return InstrumentationRegistry.getInstrumentation().acquireLooperManager(l);
213 }
214
Jason Monke7507482017-02-02 13:00:05 -0500215 private static final Map<Object, TestableLooper> sLoopers = new ArrayMap<>();
216
Jason Monk0c408002017-05-03 15:43:52 -0400217 /**
218 * For use with {@link RunWithLooper}, used to get the TestableLooper that was
219 * automatically created for this test.
220 */
Jason Monke7507482017-02-02 13:00:05 -0500221 public static TestableLooper get(Object test) {
222 return sLoopers.get(test);
223 }
224
Hall Liua853b232020-05-13 18:34:11 -0700225 public static void remove(Object test) {
226 sLoopers.remove(test);
227 }
228
Jason Monk0c408002017-05-03 15:43:52 -0400229 static class LooperFrameworkMethod extends FrameworkMethod {
Jason Monk745d0a82017-04-17 11:34:22 -0400230 private HandlerThread mHandlerThread;
Jason Monke7507482017-02-02 13:00:05 -0500231
Jason Monk745d0a82017-04-17 11:34:22 -0400232 private final TestableLooper mTestableLooper;
233 private final Looper mLooper;
234 private final Handler mHandler;
235
236 public LooperFrameworkMethod(FrameworkMethod base, boolean setAsMain, Object test) {
237 super(base.getMethod());
Jason Monke7507482017-02-02 13:00:05 -0500238 try {
Jason Monk745d0a82017-04-17 11:34:22 -0400239 mLooper = setAsMain ? Looper.getMainLooper() : createLooper();
240 mTestableLooper = new TestableLooper(mLooper, false);
Beverly433923a2020-01-22 11:23:24 -0500241 if (!setAsMain) {
242 mTestableLooper.getLooper().getThread().setName(test.getClass().getName());
243 }
Jason Monke7507482017-02-02 13:00:05 -0500244 } catch (Exception e) {
245 throw new RuntimeException(e);
246 }
Jason Monk745d0a82017-04-17 11:34:22 -0400247 sLoopers.put(test, mTestableLooper);
248 mHandler = new Handler(mLooper);
249 }
250
251 public LooperFrameworkMethod(TestableLooper other, FrameworkMethod base) {
252 super(base.getMethod());
253 mLooper = other.mLooper;
254 mTestableLooper = other;
Jason Monk1e352f42018-05-16 10:15:33 -0400255 mHandler = Handler.createAsync(mLooper);
Jason Monk745d0a82017-04-17 11:34:22 -0400256 }
257
258 public static FrameworkMethod get(FrameworkMethod base, boolean setAsMain, Object test) {
259 if (sLoopers.containsKey(test)) {
260 return new LooperFrameworkMethod(sLoopers.get(test), base);
261 }
262 return new LooperFrameworkMethod(base, setAsMain, test);
Jason Monke7507482017-02-02 13:00:05 -0500263 }
264
265 @Override
Jason Monk745d0a82017-04-17 11:34:22 -0400266 public Object invokeExplosively(Object target, Object... params) throws Throwable {
267 if (Looper.myLooper() == mLooper) {
268 // Already on the right thread from another statement, just execute then.
269 return super.invokeExplosively(target, params);
270 }
271 boolean set = mTestableLooper.mQueueWrapper == null;
272 if (set) {
Jason Monkc429f692017-06-27 13:13:49 -0400273 mTestableLooper.mQueueWrapper = acquireLooperManager(mLooper);
Jason Monk745d0a82017-04-17 11:34:22 -0400274 }
275 try {
276 Object[] ret = new Object[1];
277 // Run the execution on the looper thread.
278 Runnable execute = () -> {
279 try {
280 ret[0] = super.invokeExplosively(target, params);
281 } catch (Throwable throwable) {
282 throw new LooperException(throwable);
283 }
284 };
285 Message m = Message.obtain(mHandler, execute);
286
287 // Dispatch our message.
288 try {
289 mTestableLooper.mQueueWrapper.execute(m);
290 } catch (LooperException e) {
291 throw e.getSource();
292 } catch (RuntimeException re) {
293 // If the TestLooperManager has to post, it will wrap what it throws in a
294 // RuntimeException, make sure we grab the actual source.
295 if (re.getCause() instanceof LooperException) {
296 throw ((LooperException) re.getCause()).getSource();
297 } else {
298 throw re.getCause();
299 }
300 } finally {
301 m.recycle();
302 }
303 return ret[0];
304 } finally {
305 if (set) {
306 mTestableLooper.mQueueWrapper.release();
307 mTestableLooper.mQueueWrapper = null;
Jason Monk759e9122018-07-20 14:52:22 -0400308 if (HOLD_MAIN_THREAD && mLooper == Looper.getMainLooper()) {
Jason Monkc429f692017-06-27 13:13:49 -0400309 TestableInstrumentation.releaseMain();
310 }
Jason Monk745d0a82017-04-17 11:34:22 -0400311 }
312 }
313 }
314
315 private Looper createLooper() {
316 // TODO: Find way to share these.
317 mHandlerThread = new HandlerThread(TestableLooper.class.getSimpleName());
318 mHandlerThread.start();
319 return mHandlerThread.getLooper();
320 }
321
322 @Override
323 protected void finalize() throws Throwable {
324 super.finalize();
325 if (mHandlerThread != null) {
326 mHandlerThread.quit();
327 }
328 }
329
330 private static class LooperException extends RuntimeException {
331 private final Throwable mSource;
332
333 public LooperException(Throwable t) {
334 mSource = t;
Jason Monkf715f412017-03-14 17:16:56 -0400335 }
Jason Monkfd8f6152017-03-24 12:44:34 +0000336
Jason Monk745d0a82017-04-17 11:34:22 -0400337 public Throwable getSource() {
338 return mSource;
Jason Monk9ec0f1e2017-02-23 11:33:54 -0500339 }
Jason Monke7507482017-02-02 13:00:05 -0500340 }
341 }
342
Jason Monk0c408002017-05-03 15:43:52 -0400343 /**
344 * Callback to control the execution of messages on the looper, when set with
345 * {@link #setMessageHandler(MessageHandler)} then {@link #onMessageHandled(Message)}
346 * will get called back for every message processed on the {@link TestableLooper}.
347 */
Jason Monke7507482017-02-02 13:00:05 -0500348 public interface MessageHandler {
349 /**
350 * Return true to have the message executed and delivered to target.
351 * Return false to not execute the message and stop executing messages.
352 */
353 boolean onMessageHandled(Message m);
354 }
355}