blob: 8b4cba12b0e63e66d0a45c2b6f863e73350afbf0 [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;
23import android.support.test.InstrumentationRegistry;
Jason Monke7507482017-02-02 13:00:05 -050024import android.util.ArrayMap;
25
Jason Monk745d0a82017-04-17 11:34:22 -040026import org.junit.runners.model.FrameworkMethod;
Jason Monke7507482017-02-02 13:00:05 -050027
28import java.lang.annotation.ElementType;
29import java.lang.annotation.Retention;
30import java.lang.annotation.RetentionPolicy;
31import java.lang.annotation.Target;
Jason Monke7507482017-02-02 13:00:05 -050032import java.util.Map;
33
34/**
Jason Monk0c408002017-05-03 15:43:52 -040035 * This is a wrapper around {@link TestLooperManager} to make it easier to manage
36 * and provide an easy annotation for use with tests.
37 *
38 * @see TestableLooperTest TestableLooperTest for examples.
Jason Monke7507482017-02-02 13:00:05 -050039 */
40public class TestableLooper {
41
Jason Monk759e9122018-07-20 14:52:22 -040042 /**
43 * Whether to hold onto the main thread through all tests in an attempt to
44 * catch crashes.
45 */
46 public static final boolean HOLD_MAIN_THREAD = false;
47
Jason Monke7507482017-02-02 13:00:05 -050048 private Looper mLooper;
49 private MessageQueue mQueue;
Jason Monke7507482017-02-02 13:00:05 -050050 private MessageHandler mMessageHandler;
51
Jason Monke7507482017-02-02 13:00:05 -050052 private Handler mHandler;
Jason Monk745d0a82017-04-17 11:34:22 -040053 private Runnable mEmptyMessage;
54 private TestLooperManager mQueueWrapper;
Jason Monke7507482017-02-02 13:00:05 -050055
Jason Monk745d0a82017-04-17 11:34:22 -040056 public TestableLooper(Looper l) throws Exception {
Jason Monkc429f692017-06-27 13:13:49 -040057 this(acquireLooperManager(l), l);
Jason Monke7507482017-02-02 13:00:05 -050058 }
59
Jason Monk1e352f42018-05-16 10:15:33 -040060 private TestableLooper(TestLooperManager wrapper, Looper l) {
Jason Monk745d0a82017-04-17 11:34:22 -040061 mQueueWrapper = wrapper;
62 setupQueue(l);
63 }
64
Jason Monk1e352f42018-05-16 10:15:33 -040065 private TestableLooper(Looper looper, boolean b) {
Jason Monk745d0a82017-04-17 11:34:22 -040066 setupQueue(looper);
Jason Monke7507482017-02-02 13:00:05 -050067 }
68
69 public Looper getLooper() {
70 return mLooper;
71 }
72
Jason Monk1e352f42018-05-16 10:15:33 -040073 private void setupQueue(Looper l) {
Jason Monk745d0a82017-04-17 11:34:22 -040074 mLooper = l;
Jason Monke7507482017-02-02 13:00:05 -050075 mQueue = mLooper.getQueue();
76 mHandler = new Handler(mLooper);
77 }
78
Jason Monke7507482017-02-02 13:00:05 -050079 /**
Jason Monk0c408002017-05-03 15:43:52 -040080 * Must be called to release the looper when the test is complete, otherwise
81 * the looper will not be available for any subsequent tests. This is
82 * automatically handled for tests using {@link RunWithLooper}.
Jason Monke7507482017-02-02 13:00:05 -050083 */
Jason Monk1e352f42018-05-16 10:15:33 -040084 public void destroy() {
Jason Monk745d0a82017-04-17 11:34:22 -040085 mQueueWrapper.release();
Jason Monk759e9122018-07-20 14:52:22 -040086 if (HOLD_MAIN_THREAD && mLooper == Looper.getMainLooper()) {
Jason Monkc429f692017-06-27 13:13:49 -040087 TestableInstrumentation.releaseMain();
88 }
Jason Monke7507482017-02-02 13:00:05 -050089 }
90
Jason Monk0c408002017-05-03 15:43:52 -040091 /**
92 * Sets a callback for all messages processed on this TestableLooper.
93 *
94 * @see {@link MessageHandler}
95 */
Jason Monke7507482017-02-02 13:00:05 -050096 public void setMessageHandler(MessageHandler handler) {
97 mMessageHandler = handler;
98 }
99
100 /**
101 * Parse num messages from the message queue.
102 *
103 * @param num Number of messages to parse
104 */
105 public int processMessages(int num) {
106 for (int i = 0; i < num; i++) {
107 if (!parseMessageInt()) {
108 return i + 1;
109 }
110 }
111 return num;
112 }
113
Jason Monk0c408002017-05-03 15:43:52 -0400114 /**
115 * Process messages in the queue until no more are found.
116 */
Jason Monke7507482017-02-02 13:00:05 -0500117 public void processAllMessages() {
118 while (processQueuedMessages() != 0) ;
119 }
120
121 private int processQueuedMessages() {
122 int count = 0;
Jason Monk745d0a82017-04-17 11:34:22 -0400123 mEmptyMessage = () -> { };
124 mHandler.post(mEmptyMessage);
125 waitForMessage(mQueueWrapper, mHandler, mEmptyMessage);
Jason Monke7507482017-02-02 13:00:05 -0500126 while (parseMessageInt()) count++;
127 return count;
128 }
129
130 private boolean parseMessageInt() {
131 try {
Jason Monk745d0a82017-04-17 11:34:22 -0400132 Message result = mQueueWrapper.next();
Jason Monke7507482017-02-02 13:00:05 -0500133 if (result != null) {
134 // This is a break message.
Jason Monk745d0a82017-04-17 11:34:22 -0400135 if (result.getCallback() == mEmptyMessage) {
136 mQueueWrapper.recycle(result);
Jason Monke7507482017-02-02 13:00:05 -0500137 return false;
138 }
139
140 if (mMessageHandler != null) {
141 if (mMessageHandler.onMessageHandled(result)) {
Jason Monk1e352f42018-05-16 10:15:33 -0400142 mQueueWrapper.execute(result);
Jason Monk745d0a82017-04-17 11:34:22 -0400143 mQueueWrapper.recycle(result);
Jason Monke7507482017-02-02 13:00:05 -0500144 } else {
Jason Monk745d0a82017-04-17 11:34:22 -0400145 mQueueWrapper.recycle(result);
Jason Monke7507482017-02-02 13:00:05 -0500146 // Message handler indicated it doesn't want us to continue.
147 return false;
148 }
149 } else {
Jason Monk1e352f42018-05-16 10:15:33 -0400150 mQueueWrapper.execute(result);
Jason Monk745d0a82017-04-17 11:34:22 -0400151 mQueueWrapper.recycle(result);
Jason Monke7507482017-02-02 13:00:05 -0500152 }
153 } else {
154 // No messages, don't continue parsing
155 return false;
156 }
157 } catch (Exception e) {
158 throw new RuntimeException(e);
159 }
160 return true;
161 }
162
163 /**
164 * Runs an executable with myLooper set and processes all messages added.
165 */
166 public void runWithLooper(RunnableWithException runnable) throws Exception {
Jason Monk745d0a82017-04-17 11:34:22 -0400167 new Handler(getLooper()).post(() -> {
168 try {
169 runnable.run();
170 } catch (Exception e) {
171 throw new RuntimeException(e);
172 }
173 });
Jason Monke7507482017-02-02 13:00:05 -0500174 processAllMessages();
Jason Monke7507482017-02-02 13:00:05 -0500175 }
176
177 public interface RunnableWithException {
178 void run() throws Exception;
179 }
180
Jason Monk0c408002017-05-03 15:43:52 -0400181 /**
182 * Annotation that tells the {@link AndroidTestingRunner} to create a TestableLooper and
183 * run this test/class on that thread. The {@link TestableLooper} can be acquired using
184 * {@link #get(Object)}.
185 */
Jason Monke7507482017-02-02 13:00:05 -0500186 @Retention(RetentionPolicy.RUNTIME)
187 @Target({ElementType.METHOD, ElementType.TYPE})
188 public @interface RunWithLooper {
189 boolean setAsMainLooper() default false;
190 }
191
Jason Monk745d0a82017-04-17 11:34:22 -0400192 private static void waitForMessage(TestLooperManager queueWrapper, Handler handler,
193 Runnable execute) {
194 for (int i = 0; i < 10; i++) {
195 if (!queueWrapper.hasMessages(handler, null, execute)) {
196 try {
197 Thread.sleep(1);
198 } catch (InterruptedException e) {
199 }
200 }
201 }
202 if (!queueWrapper.hasMessages(handler, null, execute)) {
203 throw new RuntimeException("Message didn't queue...");
204 }
205 }
206
Jason Monkc429f692017-06-27 13:13:49 -0400207 private static TestLooperManager acquireLooperManager(Looper l) {
Jason Monk759e9122018-07-20 14:52:22 -0400208 if (HOLD_MAIN_THREAD && l == Looper.getMainLooper()) {
Jason Monkc429f692017-06-27 13:13:49 -0400209 TestableInstrumentation.acquireMain();
210 }
211 return InstrumentationRegistry.getInstrumentation().acquireLooperManager(l);
212 }
213
Jason Monke7507482017-02-02 13:00:05 -0500214 private static final Map<Object, TestableLooper> sLoopers = new ArrayMap<>();
215
Jason Monk0c408002017-05-03 15:43:52 -0400216 /**
217 * For use with {@link RunWithLooper}, used to get the TestableLooper that was
218 * automatically created for this test.
219 */
Jason Monke7507482017-02-02 13:00:05 -0500220 public static TestableLooper get(Object test) {
221 return sLoopers.get(test);
222 }
223
Jason Monk0c408002017-05-03 15:43:52 -0400224 static class LooperFrameworkMethod extends FrameworkMethod {
Jason Monk745d0a82017-04-17 11:34:22 -0400225 private HandlerThread mHandlerThread;
Jason Monke7507482017-02-02 13:00:05 -0500226
Jason Monk745d0a82017-04-17 11:34:22 -0400227 private final TestableLooper mTestableLooper;
228 private final Looper mLooper;
229 private final Handler mHandler;
230
231 public LooperFrameworkMethod(FrameworkMethod base, boolean setAsMain, Object test) {
232 super(base.getMethod());
Jason Monke7507482017-02-02 13:00:05 -0500233 try {
Jason Monk745d0a82017-04-17 11:34:22 -0400234 mLooper = setAsMain ? Looper.getMainLooper() : createLooper();
235 mTestableLooper = new TestableLooper(mLooper, false);
Jason Monke7507482017-02-02 13:00:05 -0500236 } catch (Exception e) {
237 throw new RuntimeException(e);
238 }
Jason Monk745d0a82017-04-17 11:34:22 -0400239 sLoopers.put(test, mTestableLooper);
240 mHandler = new Handler(mLooper);
241 }
242
243 public LooperFrameworkMethod(TestableLooper other, FrameworkMethod base) {
244 super(base.getMethod());
245 mLooper = other.mLooper;
246 mTestableLooper = other;
Jason Monk1e352f42018-05-16 10:15:33 -0400247 mHandler = Handler.createAsync(mLooper);
Jason Monk745d0a82017-04-17 11:34:22 -0400248 }
249
250 public static FrameworkMethod get(FrameworkMethod base, boolean setAsMain, Object test) {
251 if (sLoopers.containsKey(test)) {
252 return new LooperFrameworkMethod(sLoopers.get(test), base);
253 }
254 return new LooperFrameworkMethod(base, setAsMain, test);
Jason Monke7507482017-02-02 13:00:05 -0500255 }
256
257 @Override
Jason Monk745d0a82017-04-17 11:34:22 -0400258 public Object invokeExplosively(Object target, Object... params) throws Throwable {
259 if (Looper.myLooper() == mLooper) {
260 // Already on the right thread from another statement, just execute then.
261 return super.invokeExplosively(target, params);
262 }
263 boolean set = mTestableLooper.mQueueWrapper == null;
264 if (set) {
Jason Monkc429f692017-06-27 13:13:49 -0400265 mTestableLooper.mQueueWrapper = acquireLooperManager(mLooper);
Jason Monk745d0a82017-04-17 11:34:22 -0400266 }
267 try {
268 Object[] ret = new Object[1];
269 // Run the execution on the looper thread.
270 Runnable execute = () -> {
271 try {
272 ret[0] = super.invokeExplosively(target, params);
273 } catch (Throwable throwable) {
274 throw new LooperException(throwable);
275 }
276 };
277 Message m = Message.obtain(mHandler, execute);
278
279 // Dispatch our message.
280 try {
281 mTestableLooper.mQueueWrapper.execute(m);
282 } catch (LooperException e) {
283 throw e.getSource();
284 } catch (RuntimeException re) {
285 // If the TestLooperManager has to post, it will wrap what it throws in a
286 // RuntimeException, make sure we grab the actual source.
287 if (re.getCause() instanceof LooperException) {
288 throw ((LooperException) re.getCause()).getSource();
289 } else {
290 throw re.getCause();
291 }
292 } finally {
293 m.recycle();
294 }
295 return ret[0];
296 } finally {
297 if (set) {
298 mTestableLooper.mQueueWrapper.release();
299 mTestableLooper.mQueueWrapper = null;
Jason Monk759e9122018-07-20 14:52:22 -0400300 if (HOLD_MAIN_THREAD && mLooper == Looper.getMainLooper()) {
Jason Monkc429f692017-06-27 13:13:49 -0400301 TestableInstrumentation.releaseMain();
302 }
Jason Monk745d0a82017-04-17 11:34:22 -0400303 }
304 }
305 }
306
307 private Looper createLooper() {
308 // TODO: Find way to share these.
309 mHandlerThread = new HandlerThread(TestableLooper.class.getSimpleName());
310 mHandlerThread.start();
311 return mHandlerThread.getLooper();
312 }
313
314 @Override
315 protected void finalize() throws Throwable {
316 super.finalize();
317 if (mHandlerThread != null) {
318 mHandlerThread.quit();
319 }
320 }
321
322 private static class LooperException extends RuntimeException {
323 private final Throwable mSource;
324
325 public LooperException(Throwable t) {
326 mSource = t;
Jason Monkf715f412017-03-14 17:16:56 -0400327 }
Jason Monkfd8f6152017-03-24 12:44:34 +0000328
Jason Monk745d0a82017-04-17 11:34:22 -0400329 public Throwable getSource() {
330 return mSource;
Jason Monk9ec0f1e2017-02-23 11:33:54 -0500331 }
Jason Monke7507482017-02-02 13:00:05 -0500332 }
333 }
334
Jason Monk0c408002017-05-03 15:43:52 -0400335 /**
336 * Callback to control the execution of messages on the looper, when set with
337 * {@link #setMessageHandler(MessageHandler)} then {@link #onMessageHandled(Message)}
338 * will get called back for every message processed on the {@link TestableLooper}.
339 */
Jason Monke7507482017-02-02 13:00:05 -0500340 public interface MessageHandler {
341 /**
342 * Return true to have the message executed and delivered to target.
343 * Return false to not execute the message and stop executing messages.
344 */
345 boolean onMessageHandled(Message m);
346 }
347}