• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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 
15 package android.testing;
16 
17 import android.os.Handler;
18 import android.os.HandlerThread;
19 import android.os.Looper;
20 import android.os.Message;
21 import android.os.MessageQueue;
22 import android.os.TestLooperManager;
23 import android.util.ArrayMap;
24 
25 import androidx.test.InstrumentationRegistry;
26 
27 import org.junit.runners.model.FrameworkMethod;
28 
29 import java.lang.annotation.ElementType;
30 import java.lang.annotation.Retention;
31 import java.lang.annotation.RetentionPolicy;
32 import java.lang.annotation.Target;
33 import java.lang.reflect.Field;
34 import java.util.Map;
35 
36 /**
37  * This is a wrapper around {@link TestLooperManager} to make it easier to manage
38  * and provide an easy annotation for use with tests.
39  *
40  * @see TestableLooperTest TestableLooperTest for examples.
41  */
42 public class TestableLooper {
43 
44     /**
45      * Whether to hold onto the main thread through all tests in an attempt to
46      * catch crashes.
47      */
48     public static final boolean HOLD_MAIN_THREAD = false;
49     private static final Field MESSAGE_QUEUE_MESSAGES_FIELD;
50     private static final Field MESSAGE_NEXT_FIELD;
51     private static final Field MESSAGE_WHEN_FIELD;
52 
53     private Looper mLooper;
54     private MessageQueue mQueue;
55     private MessageHandler mMessageHandler;
56 
57     private Handler mHandler;
58     private Runnable mEmptyMessage;
59     private TestLooperManager mQueueWrapper;
60 
61     static {
62         try {
63             MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages");
64             MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true);
65             MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next");
66             MESSAGE_NEXT_FIELD.setAccessible(true);
67             MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when");
68             MESSAGE_WHEN_FIELD.setAccessible(true);
69         } catch (NoSuchFieldException e) {
70             throw new RuntimeException("Failed to initialize TestableLooper", e);
71         }
72     }
73 
TestableLooper(Looper l)74     public TestableLooper(Looper l) throws Exception {
75         this(acquireLooperManager(l), l);
76     }
77 
TestableLooper(TestLooperManager wrapper, Looper l)78     private TestableLooper(TestLooperManager wrapper, Looper l) {
79         mQueueWrapper = wrapper;
80         setupQueue(l);
81     }
82 
TestableLooper(Looper looper, boolean b)83     private TestableLooper(Looper looper, boolean b) {
84         setupQueue(looper);
85     }
86 
getLooper()87     public Looper getLooper() {
88         return mLooper;
89     }
90 
setupQueue(Looper l)91     private void setupQueue(Looper l) {
92         mLooper = l;
93         mQueue = mLooper.getQueue();
94         mHandler = new Handler(mLooper);
95     }
96 
97     /**
98      * Must be called to release the looper when the test is complete, otherwise
99      * the looper will not be available for any subsequent tests. This is
100      * automatically handled for tests using {@link RunWithLooper}.
101      */
destroy()102     public void destroy() {
103         mQueueWrapper.release();
104         if (HOLD_MAIN_THREAD && mLooper == Looper.getMainLooper()) {
105             TestableInstrumentation.releaseMain();
106         }
107     }
108 
109     /**
110      * Sets a callback for all messages processed on this TestableLooper.
111      *
112      * @see {@link MessageHandler}
113      */
setMessageHandler(MessageHandler handler)114     public void setMessageHandler(MessageHandler handler) {
115         mMessageHandler = handler;
116     }
117 
118     /**
119      * Parse num messages from the message queue.
120      *
121      * @param num Number of messages to parse
122      */
processMessages(int num)123     public int processMessages(int num) {
124         for (int i = 0; i < num; i++) {
125             if (!parseMessageInt()) {
126                 return i + 1;
127             }
128         }
129         return num;
130     }
131 
132     /**
133      * Process messages in the queue until no more are found.
134      */
processAllMessages()135     public void processAllMessages() {
136         while (processQueuedMessages() != 0) ;
137     }
138 
moveTimeForward(long milliSeconds)139     public void moveTimeForward(long milliSeconds) {
140         try {
141             Message msg = getMessageLinkedList();
142             while (msg != null) {
143                 long updatedWhen = msg.getWhen() - milliSeconds;
144                 if (updatedWhen < 0) {
145                     updatedWhen = 0;
146                 }
147                 MESSAGE_WHEN_FIELD.set(msg, updatedWhen);
148                 msg = (Message) MESSAGE_NEXT_FIELD.get(msg);
149             }
150         } catch (IllegalAccessException e) {
151             throw new RuntimeException("Access failed in TestableLooper: set - Message.when", e);
152         }
153     }
154 
getMessageLinkedList()155     private Message getMessageLinkedList() {
156         try {
157             MessageQueue queue = mLooper.getQueue();
158             return (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(queue);
159         } catch (IllegalAccessException e) {
160             throw new RuntimeException(
161                     "Access failed in TestableLooper: get - MessageQueue.mMessages",
162                     e);
163         }
164     }
165 
processQueuedMessages()166     private int processQueuedMessages() {
167         int count = 0;
168         mEmptyMessage = () -> { };
169         mHandler.post(mEmptyMessage);
170         waitForMessage(mQueueWrapper, mHandler, mEmptyMessage);
171         while (parseMessageInt()) count++;
172         return count;
173     }
174 
parseMessageInt()175     private boolean parseMessageInt() {
176         try {
177             Message result = mQueueWrapper.next();
178             if (result != null) {
179                 // This is a break message.
180                 if (result.getCallback() == mEmptyMessage) {
181                     mQueueWrapper.recycle(result);
182                     return false;
183                 }
184 
185                 if (mMessageHandler != null) {
186                     if (mMessageHandler.onMessageHandled(result)) {
187                         mQueueWrapper.execute(result);
188                         mQueueWrapper.recycle(result);
189                     } else {
190                         mQueueWrapper.recycle(result);
191                         // Message handler indicated it doesn't want us to continue.
192                         return false;
193                     }
194                 } else {
195                     mQueueWrapper.execute(result);
196                     mQueueWrapper.recycle(result);
197                 }
198             } else {
199                 // No messages, don't continue parsing
200                 return false;
201             }
202         } catch (Exception e) {
203             throw new RuntimeException(e);
204         }
205         return true;
206     }
207 
208     /**
209      * Runs an executable with myLooper set and processes all messages added.
210      */
runWithLooper(RunnableWithException runnable)211     public void runWithLooper(RunnableWithException runnable) throws Exception {
212         new Handler(getLooper()).post(() -> {
213             try {
214                 runnable.run();
215             } catch (Exception e) {
216                 throw new RuntimeException(e);
217             }
218         });
219         processAllMessages();
220     }
221 
222     public interface RunnableWithException {
run()223         void run() throws Exception;
224     }
225 
226     /**
227      * Annotation that tells the {@link AndroidTestingRunner} to create a TestableLooper and
228      * run this test/class on that thread. The {@link TestableLooper} can be acquired using
229      * {@link #get(Object)}.
230      */
231     @Retention(RetentionPolicy.RUNTIME)
232     @Target({ElementType.METHOD, ElementType.TYPE})
233     public @interface RunWithLooper {
setAsMainLooper()234         boolean setAsMainLooper() default false;
235     }
236 
waitForMessage(TestLooperManager queueWrapper, Handler handler, Runnable execute)237     private static void waitForMessage(TestLooperManager queueWrapper, Handler handler,
238             Runnable execute) {
239         for (int i = 0; i < 10; i++) {
240             if (!queueWrapper.hasMessages(handler, null, execute)) {
241                 try {
242                     Thread.sleep(1);
243                 } catch (InterruptedException e) {
244                 }
245             }
246         }
247         if (!queueWrapper.hasMessages(handler, null, execute)) {
248             throw new RuntimeException("Message didn't queue...");
249         }
250     }
251 
acquireLooperManager(Looper l)252     private static TestLooperManager acquireLooperManager(Looper l) {
253         if (HOLD_MAIN_THREAD && l == Looper.getMainLooper()) {
254             TestableInstrumentation.acquireMain();
255         }
256         return InstrumentationRegistry.getInstrumentation().acquireLooperManager(l);
257     }
258 
259     private static final Map<Object, TestableLooper> sLoopers = new ArrayMap<>();
260 
261     /**
262      * For use with {@link RunWithLooper}, used to get the TestableLooper that was
263      * automatically created for this test.
264      */
get(Object test)265     public static TestableLooper get(Object test) {
266         return sLoopers.get(test);
267     }
268 
remove(Object test)269     public static void remove(Object test) {
270         sLoopers.remove(test);
271     }
272 
273     static class LooperFrameworkMethod extends FrameworkMethod {
274         private HandlerThread mHandlerThread;
275 
276         private final TestableLooper mTestableLooper;
277         private final Looper mLooper;
278         private final Handler mHandler;
279 
LooperFrameworkMethod(FrameworkMethod base, boolean setAsMain, Object test)280         public LooperFrameworkMethod(FrameworkMethod base, boolean setAsMain, Object test) {
281             super(base.getMethod());
282             try {
283                 mLooper = setAsMain ? Looper.getMainLooper() : createLooper();
284                 mTestableLooper = new TestableLooper(mLooper, false);
285                 if (!setAsMain) {
286                     mTestableLooper.getLooper().getThread().setName(test.getClass().getName());
287                 }
288             } catch (Exception e) {
289                 throw new RuntimeException(e);
290             }
291             sLoopers.put(test, mTestableLooper);
292             mHandler = new Handler(mLooper);
293         }
294 
LooperFrameworkMethod(TestableLooper other, FrameworkMethod base)295         public LooperFrameworkMethod(TestableLooper other, FrameworkMethod base) {
296             super(base.getMethod());
297             mLooper = other.mLooper;
298             mTestableLooper = other;
299             mHandler = Handler.createAsync(mLooper);
300         }
301 
get(FrameworkMethod base, boolean setAsMain, Object test)302         public static FrameworkMethod get(FrameworkMethod base, boolean setAsMain, Object test) {
303             if (sLoopers.containsKey(test)) {
304                 return new LooperFrameworkMethod(sLoopers.get(test), base);
305             }
306             return new LooperFrameworkMethod(base, setAsMain, test);
307         }
308 
309         @Override
invokeExplosively(Object target, Object... params)310         public Object invokeExplosively(Object target, Object... params) throws Throwable {
311             if (Looper.myLooper() == mLooper) {
312                 // Already on the right thread from another statement, just execute then.
313                 return super.invokeExplosively(target, params);
314             }
315             boolean set = mTestableLooper.mQueueWrapper == null;
316             if (set) {
317                 mTestableLooper.mQueueWrapper = acquireLooperManager(mLooper);
318             }
319             try {
320                 Object[] ret = new Object[1];
321                 // Run the execution on the looper thread.
322                 Runnable execute = () -> {
323                     try {
324                         ret[0] = super.invokeExplosively(target, params);
325                     } catch (Throwable throwable) {
326                         throw new LooperException(throwable);
327                     }
328                 };
329                 Message m = Message.obtain(mHandler, execute);
330 
331                 // Dispatch our message.
332                 try {
333                     mTestableLooper.mQueueWrapper.execute(m);
334                 } catch (LooperException e) {
335                     throw e.getSource();
336                 } catch (RuntimeException re) {
337                     // If the TestLooperManager has to post, it will wrap what it throws in a
338                     // RuntimeException, make sure we grab the actual source.
339                     if (re.getCause() instanceof LooperException) {
340                         throw ((LooperException) re.getCause()).getSource();
341                     } else {
342                         throw re.getCause();
343                     }
344                 } finally {
345                     m.recycle();
346                 }
347                 return ret[0];
348             } finally {
349                 if (set) {
350                     mTestableLooper.mQueueWrapper.release();
351                     mTestableLooper.mQueueWrapper = null;
352                     if (HOLD_MAIN_THREAD && mLooper == Looper.getMainLooper()) {
353                         TestableInstrumentation.releaseMain();
354                     }
355                 }
356             }
357         }
358 
createLooper()359         private Looper createLooper() {
360             // TODO: Find way to share these.
361             mHandlerThread = new HandlerThread(TestableLooper.class.getSimpleName());
362             mHandlerThread.start();
363             return mHandlerThread.getLooper();
364         }
365 
366         @Override
finalize()367         protected void finalize() throws Throwable {
368             super.finalize();
369             if (mHandlerThread != null) {
370                 mHandlerThread.quit();
371             }
372         }
373 
374         private static class LooperException extends RuntimeException {
375             private final Throwable mSource;
376 
LooperException(Throwable t)377             public LooperException(Throwable t) {
378                 mSource = t;
379             }
380 
getSource()381             public Throwable getSource() {
382                 return mSource;
383             }
384         }
385     }
386 
387     /**
388      * Callback to control the execution of messages on the looper, when set with
389      * {@link #setMessageHandler(MessageHandler)} then {@link #onMessageHandled(Message)}
390      * will get called back for every message processed on the {@link TestableLooper}.
391      */
392     public interface MessageHandler {
393         /**
394          * Return true to have the message executed and delivered to target.
395          * Return false to not execute the message and stop executing messages.
396          */
onMessageHandled(Message m)397         boolean onMessageHandled(Message m);
398     }
399 }
400