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             } catch (Exception e) {
242                 throw new RuntimeException(e);
243             }
244             sLoopers.put(test, mTestableLooper);
245             mHandler = new Handler(mLooper);
246         }
247 
LooperFrameworkMethod(TestableLooper other, FrameworkMethod base)248         public LooperFrameworkMethod(TestableLooper other, FrameworkMethod base) {
249             super(base.getMethod());
250             mLooper = other.mLooper;
251             mTestableLooper = other;
252             mHandler = Handler.createAsync(mLooper);
253         }
254 
get(FrameworkMethod base, boolean setAsMain, Object test)255         public static FrameworkMethod get(FrameworkMethod base, boolean setAsMain, Object test) {
256             if (sLoopers.containsKey(test)) {
257                 return new LooperFrameworkMethod(sLoopers.get(test), base);
258             }
259             return new LooperFrameworkMethod(base, setAsMain, test);
260         }
261 
262         @Override
invokeExplosively(Object target, Object... params)263         public Object invokeExplosively(Object target, Object... params) throws Throwable {
264             if (Looper.myLooper() == mLooper) {
265                 // Already on the right thread from another statement, just execute then.
266                 return super.invokeExplosively(target, params);
267             }
268             boolean set = mTestableLooper.mQueueWrapper == null;
269             if (set) {
270                 mTestableLooper.mQueueWrapper = acquireLooperManager(mLooper);
271             }
272             try {
273                 Object[] ret = new Object[1];
274                 // Run the execution on the looper thread.
275                 Runnable execute = () -> {
276                     try {
277                         ret[0] = super.invokeExplosively(target, params);
278                     } catch (Throwable throwable) {
279                         throw new LooperException(throwable);
280                     }
281                 };
282                 Message m = Message.obtain(mHandler, execute);
283 
284                 // Dispatch our message.
285                 try {
286                     mTestableLooper.mQueueWrapper.execute(m);
287                 } catch (LooperException e) {
288                     throw e.getSource();
289                 } catch (RuntimeException re) {
290                     // If the TestLooperManager has to post, it will wrap what it throws in a
291                     // RuntimeException, make sure we grab the actual source.
292                     if (re.getCause() instanceof LooperException) {
293                         throw ((LooperException) re.getCause()).getSource();
294                     } else {
295                         throw re.getCause();
296                     }
297                 } finally {
298                     m.recycle();
299                 }
300                 return ret[0];
301             } finally {
302                 if (set) {
303                     mTestableLooper.mQueueWrapper.release();
304                     mTestableLooper.mQueueWrapper = null;
305                     if (HOLD_MAIN_THREAD && mLooper == Looper.getMainLooper()) {
306                         TestableInstrumentation.releaseMain();
307                     }
308                 }
309             }
310         }
311 
createLooper()312         private Looper createLooper() {
313             // TODO: Find way to share these.
314             mHandlerThread = new HandlerThread(TestableLooper.class.getSimpleName());
315             mHandlerThread.start();
316             return mHandlerThread.getLooper();
317         }
318 
319         @Override
finalize()320         protected void finalize() throws Throwable {
321             super.finalize();
322             if (mHandlerThread != null) {
323                 mHandlerThread.quit();
324             }
325         }
326 
327         private static class LooperException extends RuntimeException {
328             private final Throwable mSource;
329 
LooperException(Throwable t)330             public LooperException(Throwable t) {
331                 mSource = t;
332             }
333 
getSource()334             public Throwable getSource() {
335                 return mSource;
336             }
337         }
338     }
339 
340     /**
341      * Callback to control the execution of messages on the looper, when set with
342      * {@link #setMessageHandler(MessageHandler)} then {@link #onMessageHandled(Message)}
343      * will get called back for every message processed on the {@link TestableLooper}.
344      */
345     public interface MessageHandler {
346         /**
347          * Return true to have the message executed and delivered to target.
348          * Return false to not execute the message and stop executing messages.
349          */
onMessageHandled(Message m)350         boolean onMessageHandled(Message m);
351     }
352 }
353