blob: f6c3cb3ec4981cb9944bde307dc607de63a3fbfc [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.lang.reflect.Field;
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 Monke7507482017-02-02 13:00:05 -050043 private Looper mLooper;
44 private MessageQueue mQueue;
Jason Monke7507482017-02-02 13:00:05 -050045 private MessageHandler mMessageHandler;
46
Jason Monke7507482017-02-02 13:00:05 -050047 private Handler mHandler;
Jason Monk745d0a82017-04-17 11:34:22 -040048 private Runnable mEmptyMessage;
49 private TestLooperManager mQueueWrapper;
Jason Monke7507482017-02-02 13:00:05 -050050
Jason Monk745d0a82017-04-17 11:34:22 -040051 public TestableLooper(Looper l) throws Exception {
52 this(InstrumentationRegistry.getInstrumentation().acquireLooperManager(l), l);
Jason Monke7507482017-02-02 13:00:05 -050053 }
54
Jason Monk745d0a82017-04-17 11:34:22 -040055 private TestableLooper(TestLooperManager wrapper, Looper l) throws Exception {
56 mQueueWrapper = wrapper;
57 setupQueue(l);
58 }
59
60 private TestableLooper(Looper looper, boolean b) throws Exception {
61 setupQueue(looper);
Jason Monke7507482017-02-02 13:00:05 -050062 }
63
64 public Looper getLooper() {
65 return mLooper;
66 }
67
Jason Monk745d0a82017-04-17 11:34:22 -040068 private void setupQueue(Looper l) throws Exception {
69 mLooper = l;
Jason Monke7507482017-02-02 13:00:05 -050070 mQueue = mLooper.getQueue();
71 mHandler = new Handler(mLooper);
72 }
73
Jason Monke7507482017-02-02 13:00:05 -050074 /**
Jason Monk0c408002017-05-03 15:43:52 -040075 * Must be called to release the looper when the test is complete, otherwise
76 * the looper will not be available for any subsequent tests. This is
77 * automatically handled for tests using {@link RunWithLooper}.
Jason Monke7507482017-02-02 13:00:05 -050078 */
79 public void destroy() throws NoSuchFieldException, IllegalAccessException {
Jason Monk745d0a82017-04-17 11:34:22 -040080 mQueueWrapper.release();
Jason Monke7507482017-02-02 13:00:05 -050081 }
82
Jason Monk0c408002017-05-03 15:43:52 -040083 /**
84 * Sets a callback for all messages processed on this TestableLooper.
85 *
86 * @see {@link MessageHandler}
87 */
Jason Monke7507482017-02-02 13:00:05 -050088 public void setMessageHandler(MessageHandler handler) {
89 mMessageHandler = handler;
90 }
91
92 /**
93 * Parse num messages from the message queue.
94 *
95 * @param num Number of messages to parse
96 */
97 public int processMessages(int num) {
98 for (int i = 0; i < num; i++) {
99 if (!parseMessageInt()) {
100 return i + 1;
101 }
102 }
103 return num;
104 }
105
Jason Monk0c408002017-05-03 15:43:52 -0400106 /**
107 * Process messages in the queue until no more are found.
108 */
Jason Monke7507482017-02-02 13:00:05 -0500109 public void processAllMessages() {
110 while (processQueuedMessages() != 0) ;
111 }
112
113 private int processQueuedMessages() {
114 int count = 0;
Jason Monk745d0a82017-04-17 11:34:22 -0400115 mEmptyMessage = () -> { };
116 mHandler.post(mEmptyMessage);
117 waitForMessage(mQueueWrapper, mHandler, mEmptyMessage);
Jason Monke7507482017-02-02 13:00:05 -0500118 while (parseMessageInt()) count++;
119 return count;
120 }
121
122 private boolean parseMessageInt() {
123 try {
Jason Monk745d0a82017-04-17 11:34:22 -0400124 Message result = mQueueWrapper.next();
Jason Monke7507482017-02-02 13:00:05 -0500125 if (result != null) {
126 // This is a break message.
Jason Monk745d0a82017-04-17 11:34:22 -0400127 if (result.getCallback() == mEmptyMessage) {
128 mQueueWrapper.recycle(result);
Jason Monke7507482017-02-02 13:00:05 -0500129 return false;
130 }
131
132 if (mMessageHandler != null) {
133 if (mMessageHandler.onMessageHandled(result)) {
134 result.getTarget().dispatchMessage(result);
Jason Monk745d0a82017-04-17 11:34:22 -0400135 mQueueWrapper.recycle(result);
Jason Monke7507482017-02-02 13:00:05 -0500136 } else {
Jason Monk745d0a82017-04-17 11:34:22 -0400137 mQueueWrapper.recycle(result);
Jason Monke7507482017-02-02 13:00:05 -0500138 // Message handler indicated it doesn't want us to continue.
139 return false;
140 }
141 } else {
142 result.getTarget().dispatchMessage(result);
Jason Monk745d0a82017-04-17 11:34:22 -0400143 mQueueWrapper.recycle(result);
Jason Monke7507482017-02-02 13:00:05 -0500144 }
145 } else {
146 // No messages, don't continue parsing
147 return false;
148 }
149 } catch (Exception e) {
150 throw new RuntimeException(e);
151 }
152 return true;
153 }
154
155 /**
156 * Runs an executable with myLooper set and processes all messages added.
157 */
158 public void runWithLooper(RunnableWithException runnable) throws Exception {
Jason Monk745d0a82017-04-17 11:34:22 -0400159 new Handler(getLooper()).post(() -> {
160 try {
161 runnable.run();
162 } catch (Exception e) {
163 throw new RuntimeException(e);
164 }
165 });
Jason Monke7507482017-02-02 13:00:05 -0500166 processAllMessages();
Jason Monke7507482017-02-02 13:00:05 -0500167 }
168
169 public interface RunnableWithException {
170 void run() throws Exception;
171 }
172
Jason Monk0c408002017-05-03 15:43:52 -0400173 /**
174 * Annotation that tells the {@link AndroidTestingRunner} to create a TestableLooper and
175 * run this test/class on that thread. The {@link TestableLooper} can be acquired using
176 * {@link #get(Object)}.
177 */
Jason Monke7507482017-02-02 13:00:05 -0500178 @Retention(RetentionPolicy.RUNTIME)
179 @Target({ElementType.METHOD, ElementType.TYPE})
180 public @interface RunWithLooper {
181 boolean setAsMainLooper() default false;
182 }
183
Jason Monk745d0a82017-04-17 11:34:22 -0400184 private static void waitForMessage(TestLooperManager queueWrapper, Handler handler,
185 Runnable execute) {
186 for (int i = 0; i < 10; i++) {
187 if (!queueWrapper.hasMessages(handler, null, execute)) {
188 try {
189 Thread.sleep(1);
190 } catch (InterruptedException e) {
191 }
192 }
193 }
194 if (!queueWrapper.hasMessages(handler, null, execute)) {
195 throw new RuntimeException("Message didn't queue...");
196 }
197 }
198
Jason Monke7507482017-02-02 13:00:05 -0500199 private static final Map<Object, TestableLooper> sLoopers = new ArrayMap<>();
200
Jason Monk0c408002017-05-03 15:43:52 -0400201 /**
202 * For use with {@link RunWithLooper}, used to get the TestableLooper that was
203 * automatically created for this test.
204 */
Jason Monke7507482017-02-02 13:00:05 -0500205 public static TestableLooper get(Object test) {
206 return sLoopers.get(test);
207 }
208
Jason Monk0c408002017-05-03 15:43:52 -0400209 static class LooperFrameworkMethod extends FrameworkMethod {
Jason Monk745d0a82017-04-17 11:34:22 -0400210 private HandlerThread mHandlerThread;
Jason Monke7507482017-02-02 13:00:05 -0500211
Jason Monk745d0a82017-04-17 11:34:22 -0400212 private final TestableLooper mTestableLooper;
213 private final Looper mLooper;
214 private final Handler mHandler;
215
216 public LooperFrameworkMethod(FrameworkMethod base, boolean setAsMain, Object test) {
217 super(base.getMethod());
Jason Monke7507482017-02-02 13:00:05 -0500218 try {
Jason Monk745d0a82017-04-17 11:34:22 -0400219 mLooper = setAsMain ? Looper.getMainLooper() : createLooper();
220 mTestableLooper = new TestableLooper(mLooper, false);
Jason Monke7507482017-02-02 13:00:05 -0500221 } catch (Exception e) {
222 throw new RuntimeException(e);
223 }
Jason Monk745d0a82017-04-17 11:34:22 -0400224 sLoopers.put(test, mTestableLooper);
225 mHandler = new Handler(mLooper);
226 }
227
228 public LooperFrameworkMethod(TestableLooper other, FrameworkMethod base) {
229 super(base.getMethod());
230 mLooper = other.mLooper;
231 mTestableLooper = other;
232 mHandler = new Handler(mLooper);
233 }
234
235 public static FrameworkMethod get(FrameworkMethod base, boolean setAsMain, Object test) {
236 if (sLoopers.containsKey(test)) {
237 return new LooperFrameworkMethod(sLoopers.get(test), base);
238 }
239 return new LooperFrameworkMethod(base, setAsMain, test);
Jason Monke7507482017-02-02 13:00:05 -0500240 }
241
242 @Override
Jason Monk745d0a82017-04-17 11:34:22 -0400243 public Object invokeExplosively(Object target, Object... params) throws Throwable {
244 if (Looper.myLooper() == mLooper) {
245 // Already on the right thread from another statement, just execute then.
246 return super.invokeExplosively(target, params);
247 }
248 boolean set = mTestableLooper.mQueueWrapper == null;
249 if (set) {
250 mTestableLooper.mQueueWrapper = InstrumentationRegistry.getInstrumentation()
251 .acquireLooperManager(mLooper);
252 }
253 try {
254 Object[] ret = new Object[1];
255 // Run the execution on the looper thread.
256 Runnable execute = () -> {
257 try {
258 ret[0] = super.invokeExplosively(target, params);
259 } catch (Throwable throwable) {
260 throw new LooperException(throwable);
261 }
262 };
263 Message m = Message.obtain(mHandler, execute);
264
265 // Dispatch our message.
266 try {
267 mTestableLooper.mQueueWrapper.execute(m);
268 } catch (LooperException e) {
269 throw e.getSource();
270 } catch (RuntimeException re) {
271 // If the TestLooperManager has to post, it will wrap what it throws in a
272 // RuntimeException, make sure we grab the actual source.
273 if (re.getCause() instanceof LooperException) {
274 throw ((LooperException) re.getCause()).getSource();
275 } else {
276 throw re.getCause();
277 }
278 } finally {
279 m.recycle();
280 }
281 return ret[0];
282 } finally {
283 if (set) {
284 mTestableLooper.mQueueWrapper.release();
285 mTestableLooper.mQueueWrapper = null;
286 }
287 }
288 }
289
290 private Looper createLooper() {
291 // TODO: Find way to share these.
292 mHandlerThread = new HandlerThread(TestableLooper.class.getSimpleName());
293 mHandlerThread.start();
294 return mHandlerThread.getLooper();
295 }
296
297 @Override
298 protected void finalize() throws Throwable {
299 super.finalize();
300 if (mHandlerThread != null) {
301 mHandlerThread.quit();
302 }
303 }
304
305 private static class LooperException extends RuntimeException {
306 private final Throwable mSource;
307
308 public LooperException(Throwable t) {
309 mSource = t;
Jason Monkf715f412017-03-14 17:16:56 -0400310 }
Jason Monkfd8f6152017-03-24 12:44:34 +0000311
Jason Monk745d0a82017-04-17 11:34:22 -0400312 public Throwable getSource() {
313 return mSource;
Jason Monk9ec0f1e2017-02-23 11:33:54 -0500314 }
Jason Monke7507482017-02-02 13:00:05 -0500315 }
316 }
317
Jason Monk0c408002017-05-03 15:43:52 -0400318 /**
319 * Callback to control the execution of messages on the looper, when set with
320 * {@link #setMessageHandler(MessageHandler)} then {@link #onMessageHandled(Message)}
321 * will get called back for every message processed on the {@link TestableLooper}.
322 */
Jason Monke7507482017-02-02 13:00:05 -0500323 public interface MessageHandler {
324 /**
325 * Return true to have the message executed and delivered to target.
326 * Return false to not execute the message and stop executing messages.
327 */
328 boolean onMessageHandled(Message m);
329 }
330}