blob: 8d99ac7100eb3aa3743da11904957d173f864e9b [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
Jason Monk0c408002017-05-03 15:43:52 -0400225 static class LooperFrameworkMethod extends FrameworkMethod {
Jason Monk745d0a82017-04-17 11:34:22 -0400226 private HandlerThread mHandlerThread;
Jason Monke7507482017-02-02 13:00:05 -0500227
Jason Monk745d0a82017-04-17 11:34:22 -0400228 private final TestableLooper mTestableLooper;
229 private final Looper mLooper;
230 private final Handler mHandler;
231
232 public LooperFrameworkMethod(FrameworkMethod base, boolean setAsMain, Object test) {
233 super(base.getMethod());
Jason Monke7507482017-02-02 13:00:05 -0500234 try {
Jason Monk745d0a82017-04-17 11:34:22 -0400235 mLooper = setAsMain ? Looper.getMainLooper() : createLooper();
236 mTestableLooper = new TestableLooper(mLooper, false);
Jason Monke7507482017-02-02 13:00:05 -0500237 } catch (Exception e) {
238 throw new RuntimeException(e);
239 }
Jason Monk745d0a82017-04-17 11:34:22 -0400240 sLoopers.put(test, mTestableLooper);
241 mHandler = new Handler(mLooper);
242 }
243
244 public LooperFrameworkMethod(TestableLooper other, FrameworkMethod base) {
245 super(base.getMethod());
246 mLooper = other.mLooper;
247 mTestableLooper = other;
Jason Monk1e352f42018-05-16 10:15:33 -0400248 mHandler = Handler.createAsync(mLooper);
Jason Monk745d0a82017-04-17 11:34:22 -0400249 }
250
251 public static FrameworkMethod get(FrameworkMethod base, boolean setAsMain, Object test) {
252 if (sLoopers.containsKey(test)) {
253 return new LooperFrameworkMethod(sLoopers.get(test), base);
254 }
255 return new LooperFrameworkMethod(base, setAsMain, test);
Jason Monke7507482017-02-02 13:00:05 -0500256 }
257
258 @Override
Jason Monk745d0a82017-04-17 11:34:22 -0400259 public Object invokeExplosively(Object target, Object... params) throws Throwable {
260 if (Looper.myLooper() == mLooper) {
261 // Already on the right thread from another statement, just execute then.
262 return super.invokeExplosively(target, params);
263 }
264 boolean set = mTestableLooper.mQueueWrapper == null;
265 if (set) {
Jason Monkc429f692017-06-27 13:13:49 -0400266 mTestableLooper.mQueueWrapper = acquireLooperManager(mLooper);
Jason Monk745d0a82017-04-17 11:34:22 -0400267 }
268 try {
269 Object[] ret = new Object[1];
270 // Run the execution on the looper thread.
271 Runnable execute = () -> {
272 try {
273 ret[0] = super.invokeExplosively(target, params);
274 } catch (Throwable throwable) {
275 throw new LooperException(throwable);
276 }
277 };
278 Message m = Message.obtain(mHandler, execute);
279
280 // Dispatch our message.
281 try {
282 mTestableLooper.mQueueWrapper.execute(m);
283 } catch (LooperException e) {
284 throw e.getSource();
285 } catch (RuntimeException re) {
286 // If the TestLooperManager has to post, it will wrap what it throws in a
287 // RuntimeException, make sure we grab the actual source.
288 if (re.getCause() instanceof LooperException) {
289 throw ((LooperException) re.getCause()).getSource();
290 } else {
291 throw re.getCause();
292 }
293 } finally {
294 m.recycle();
295 }
296 return ret[0];
297 } finally {
298 if (set) {
299 mTestableLooper.mQueueWrapper.release();
300 mTestableLooper.mQueueWrapper = null;
Jason Monk759e9122018-07-20 14:52:22 -0400301 if (HOLD_MAIN_THREAD && mLooper == Looper.getMainLooper()) {
Jason Monkc429f692017-06-27 13:13:49 -0400302 TestableInstrumentation.releaseMain();
303 }
Jason Monk745d0a82017-04-17 11:34:22 -0400304 }
305 }
306 }
307
308 private Looper createLooper() {
309 // TODO: Find way to share these.
310 mHandlerThread = new HandlerThread(TestableLooper.class.getSimpleName());
311 mHandlerThread.start();
312 return mHandlerThread.getLooper();
313 }
314
315 @Override
316 protected void finalize() throws Throwable {
317 super.finalize();
318 if (mHandlerThread != null) {
319 mHandlerThread.quit();
320 }
321 }
322
323 private static class LooperException extends RuntimeException {
324 private final Throwable mSource;
325
326 public LooperException(Throwable t) {
327 mSource = t;
Jason Monkf715f412017-03-14 17:16:56 -0400328 }
Jason Monkfd8f6152017-03-24 12:44:34 +0000329
Jason Monk745d0a82017-04-17 11:34:22 -0400330 public Throwable getSource() {
331 return mSource;
Jason Monk9ec0f1e2017-02-23 11:33:54 -0500332 }
Jason Monke7507482017-02-02 13:00:05 -0500333 }
334 }
335
Jason Monk0c408002017-05-03 15:43:52 -0400336 /**
337 * Callback to control the execution of messages on the looper, when set with
338 * {@link #setMessageHandler(MessageHandler)} then {@link #onMessageHandled(Message)}
339 * will get called back for every message processed on the {@link TestableLooper}.
340 */
Jason Monke7507482017-02-02 13:00:05 -0500341 public interface MessageHandler {
342 /**
343 * Return true to have the message executed and delivered to target.
344 * Return false to not execute the message and stop executing messages.
345 */
346 boolean onMessageHandled(Message m);
347 }
348}