• 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.util.Map;
34 
35 /**
36  * 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.
40  */
41 public class TestableLooper {
42 
43     /**
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 
49     private Looper mLooper;
50     private MessageQueue mQueue;
51     private MessageHandler mMessageHandler;
52 
53     private Handler mHandler;
54     private Runnable mEmptyMessage;
55     private TestLooperManager mQueueWrapper;
56 
TestableLooper(Looper l)57     public TestableLooper(Looper l) throws Exception {
58         this(acquireLooperManager(l), l);
59     }
60 
TestableLooper(TestLooperManager wrapper, Looper l)61     private TestableLooper(TestLooperManager wrapper, Looper l) {
62         mQueueWrapper = wrapper;
63         setupQueue(l);
64     }
65 
TestableLooper(Looper looper, boolean b)66     private TestableLooper(Looper looper, boolean b) {
67         setupQueue(looper);
68     }
69 
getLooper()70     public Looper getLooper() {
71         return mLooper;
72     }
73 
setupQueue(Looper l)74     private void setupQueue(Looper l) {
75         mLooper = l;
76         mQueue = mLooper.getQueue();
77         mHandler = new Handler(mLooper);
78     }
79 
80     /**
81      * 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}.
84      */
destroy()85     public void destroy() {
86         mQueueWrapper.release();
87         if (HOLD_MAIN_THREAD && mLooper == Looper.getMainLooper()) {
88             TestableInstrumentation.releaseMain();
89         }
90     }
91 
92     /**
93      * Sets a callback for all messages processed on this TestableLooper.
94      *
95      * @see {@link MessageHandler}
96      */
setMessageHandler(MessageHandler handler)97     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      */
processMessages(int num)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 
115     /**
116      * Process messages in the queue until no more are found.
117      */
processAllMessages()118     public void processAllMessages() {
119         while (processQueuedMessages() != 0) ;
120     }
121 
processQueuedMessages()122     private int processQueuedMessages() {
123         int count = 0;
124         mEmptyMessage = () -> { };
125         mHandler.post(mEmptyMessage);
126         waitForMessage(mQueueWrapper, mHandler, mEmptyMessage);
127         while (parseMessageInt()) count++;
128         return count;
129     }
130 
parseMessageInt()131     private boolean parseMessageInt() {
132         try {
133             Message result = mQueueWrapper.next();
134             if (result != null) {
135                 // This is a break message.
136                 if (result.getCallback() == mEmptyMessage) {
137                     mQueueWrapper.recycle(result);
138                     return false;
139                 }
140 
141                 if (mMessageHandler != null) {
142                     if (mMessageHandler.onMessageHandled(result)) {
143                         mQueueWrapper.execute(result);
144                         mQueueWrapper.recycle(result);
145                     } else {
146                         mQueueWrapper.recycle(result);
147                         // Message handler indicated it doesn't want us to continue.
148                         return false;
149                     }
150                 } else {
151                     mQueueWrapper.execute(result);
152                     mQueueWrapper.recycle(result);
153                 }
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      */
runWithLooper(RunnableWithException runnable)167     public void runWithLooper(RunnableWithException runnable) throws Exception {
168         new Handler(getLooper()).post(() -> {
169             try {
170                 runnable.run();
171             } catch (Exception e) {
172                 throw new RuntimeException(e);
173             }
174         });
175         processAllMessages();
176     }
177 
178     public interface RunnableWithException {
run()179         void run() throws Exception;
180     }
181 
182     /**
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      */
187     @Retention(RetentionPolicy.RUNTIME)
188     @Target({ElementType.METHOD, ElementType.TYPE})
189     public @interface RunWithLooper {
setAsMainLooper()190         boolean setAsMainLooper() default false;
191     }
192 
waitForMessage(TestLooperManager queueWrapper, Handler handler, Runnable execute)193     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 
acquireLooperManager(Looper l)208     private static TestLooperManager acquireLooperManager(Looper l) {
209         if (HOLD_MAIN_THREAD && l == Looper.getMainLooper()) {
210             TestableInstrumentation.acquireMain();
211         }
212         return InstrumentationRegistry.getInstrumentation().acquireLooperManager(l);
213     }
214 
215     private static final Map<Object, TestableLooper> sLoopers = new ArrayMap<>();
216 
217     /**
218      * For use with {@link RunWithLooper}, used to get the TestableLooper that was
219      * automatically created for this test.
220      */
get(Object test)221     public static TestableLooper get(Object test) {
222         return sLoopers.get(test);
223     }
224 
remove(Object test)225     public static void remove(Object test) {
226         sLoopers.remove(test);
227     }
228 
229     static class LooperFrameworkMethod extends FrameworkMethod {
230         private HandlerThread mHandlerThread;
231 
232         private final TestableLooper mTestableLooper;
233         private final Looper mLooper;
234         private final Handler mHandler;
235 
LooperFrameworkMethod(FrameworkMethod base, boolean setAsMain, Object test)236         public LooperFrameworkMethod(FrameworkMethod base, boolean setAsMain, Object test) {
237             super(base.getMethod());
238             try {
239                 mLooper = setAsMain ? Looper.getMainLooper() : createLooper();
240                 mTestableLooper = new TestableLooper(mLooper, false);
241                 if (!setAsMain) {
242                     mTestableLooper.getLooper().getThread().setName(test.getClass().getName());
243                 }
244             } catch (Exception e) {
245                 throw new RuntimeException(e);
246             }
247             sLoopers.put(test, mTestableLooper);
248             mHandler = new Handler(mLooper);
249         }
250 
LooperFrameworkMethod(TestableLooper other, FrameworkMethod base)251         public LooperFrameworkMethod(TestableLooper other, FrameworkMethod base) {
252             super(base.getMethod());
253             mLooper = other.mLooper;
254             mTestableLooper = other;
255             mHandler = Handler.createAsync(mLooper);
256         }
257 
get(FrameworkMethod base, boolean setAsMain, Object test)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);
263         }
264 
265         @Override
invokeExplosively(Object target, Object... params)266         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) {
273                 mTestableLooper.mQueueWrapper = acquireLooperManager(mLooper);
274             }
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;
308                     if (HOLD_MAIN_THREAD && mLooper == Looper.getMainLooper()) {
309                         TestableInstrumentation.releaseMain();
310                     }
311                 }
312             }
313         }
314 
createLooper()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
finalize()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 
LooperException(Throwable t)333             public LooperException(Throwable t) {
334                 mSource = t;
335             }
336 
getSource()337             public Throwable getSource() {
338                 return mSource;
339             }
340         }
341     }
342 
343     /**
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      */
348     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          */
onMessageHandled(Message m)353         boolean onMessageHandled(Message m);
354     }
355 }
356