• 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.annotation.NonNull;
18 import android.annotation.Nullable;
19 import android.os.Build;
20 import android.os.Handler;
21 import android.os.HandlerThread;
22 import android.os.Looper;
23 import android.os.Message;
24 import android.os.MessageQueue;
25 import android.os.TestLooperManager;
26 import android.util.ArrayMap;
27 
28 import androidx.test.platform.app.InstrumentationRegistry;
29 
30 import org.junit.runners.model.FrameworkMethod;
31 
32 import java.lang.annotation.ElementType;
33 import java.lang.annotation.Retention;
34 import java.lang.annotation.RetentionPolicy;
35 import java.lang.annotation.Target;
36 import java.lang.reflect.Field;
37 import java.lang.reflect.Method;
38 import java.util.ArrayDeque;
39 import java.util.Map;
40 import java.util.Objects;
41 import java.util.Queue;
42 import java.util.concurrent.atomic.AtomicBoolean;
43 
44 /**
45  * This is a wrapper around {@link TestLooperManager} to make it easier to manage
46  * and provide an easy annotation for use with tests.
47  *
48  * @see TestableLooperTest TestableLooperTest for examples.
49  *
50  * @deprecated Use {@link android.os.TestLooperManager} or {@link
51  *     org.robolectric.shadows.ShadowLooper} instead.
52  *     This class is not actively maintained.
53  *     Both of the recommended alternatives allow fine control of execution.
54  *     The Robolectric class also allows advancing time.
55  */
56 public class TestableLooper {
57 
58     /**
59      * Whether to hold onto the main thread through all tests in an attempt to
60      * catch crashes.
61      */
62     public static final boolean HOLD_MAIN_THREAD = false;
63     private static final Field MESSAGE_QUEUE_MESSAGES_FIELD;
64     private static final Field MESSAGE_NEXT_FIELD;
65     private static final Field MESSAGE_WHEN_FIELD;
66 
67     private Looper mLooper;
68     private MessageQueue mQueue;
69     private MessageHandler mMessageHandler;
70 
71     private Handler mHandler;
72     private TestLooperManager mQueueWrapper;
73 
74     /**
75      * Baklava introduces new {@link TestLooperManager} APIs that we can use instead of reflection.
76      */
isAtLeastBaklava()77     private static boolean isAtLeastBaklava() {
78         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA;
79     }
80 
81     static {
82         if (isAtLeastBaklava()) {
83             MESSAGE_QUEUE_MESSAGES_FIELD = null;
84             MESSAGE_NEXT_FIELD = null;
85             MESSAGE_WHEN_FIELD = null;
86         } else {
87             try {
88                 MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages");
89                 MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true);
90                 MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next");
91                 MESSAGE_NEXT_FIELD.setAccessible(true);
92                 MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when");
93                 MESSAGE_WHEN_FIELD.setAccessible(true);
94             } catch (NoSuchFieldException e) {
95                 throw new RuntimeException("Failed to initialize TestableLooper", e);
96             }
97         }
98     }
99 
TestableLooper(Looper l)100     public TestableLooper(Looper l) throws Exception {
101         this(acquireLooperManager(l), l);
102     }
103 
TestableLooper(TestLooperManager wrapper, Looper l)104     private TestableLooper(TestLooperManager wrapper, Looper l) {
105         mQueueWrapper = Objects.requireNonNull(wrapper);
106         setupQueue(l);
107     }
108 
TestableLooper(Looper looper, boolean b)109     private TestableLooper(Looper looper, boolean b) {
110         setupQueue(looper);
111     }
112 
113     /**
114      * Wrap the given runnable so that it will run blocking on the Looper that will be set up for
115      * the given test.
116      * <p>
117      * This method is required to support any TestRule which needs to run setup and/or teardown code
118      * on the TestableLooper. Whether using {@link AndroidTestingRunner} or
119      * {@link TestWithLooperRule}, the TestRule's Statement evaluates on the test instrumentation
120      * thread, rather than the TestableLooper thread, so access to the TestableLooper is required.
121      * However, {@link #get(Object)} will return {@code null} both before and after the inner
122      * statement is evaluated:
123      * <ul>
124      * <li>Before the test {@link #get} returns {@code null} because while the TestableLooperHolder
125      * is accessible in sLoopers, it has not been initialized with an actual TestableLooper yet.
126      * This method's use of the internal LooperFrameworkMethod ensures that all setup and teardown
127      * of the TestableLooper happen as it would for all other wrapped code blocks.
128      * <li>After the test {@link #get} can return {@code null} because many tests call
129      * {@link #remove} in the teardown method. The fact that this method returns a runnable allows
130      * it to be called before the test (when the TestableLooperHolder is still in sLoopers), and
131      * then executed as teardown after the test.
132      * </ul>
133      *
134      * @param test     the test instance (just like passed to {@link #get(Object)})
135      * @param runnable the operation that should eventually be run on the TestableLooper
136      * @return a runnable that will block the thread on which it is called until the given runnable
137      *          is finished.  Will be {@code null} if there is no looper for the given test.
138      * @hide
139      */
140     @Nullable
wrapWithRunBlocking( Object test, @NonNull RunnableWithException runnable)141     public static RunnableWithException wrapWithRunBlocking(
142             Object test, @NonNull RunnableWithException runnable) {
143         TestableLooperHolder looperHolder = sLoopers.get(test);
144         if (looperHolder == null) {
145             return null;
146         }
147         try {
148             FrameworkMethod base = new FrameworkMethod(runnable.getClass().getMethod("run"));
149             LooperFrameworkMethod wrapped = new LooperFrameworkMethod(base, looperHolder);
150             return () -> {
151                 try {
152                     wrapped.invokeExplosively(runnable);
153                 } catch (RuntimeException | Error e) {
154                     throw e;
155                 } catch (Throwable e) {
156                     throw new RuntimeException(e);
157                 }
158             };
159         } catch (NoSuchMethodException e) {
160             throw new RuntimeException(e);
161         }
162     }
163 
164     public Looper getLooper() {
165         return mLooper;
166     }
167 
168     private void setupQueue(Looper l) {
169         mLooper = l;
170         mQueue = mLooper.getQueue();
171         mHandler = new Handler(mLooper);
172     }
173 
174     /**
175      * Must be called to release the looper when the test is complete, otherwise
176      * the looper will not be available for any subsequent tests. This is
177      * automatically handled for tests using {@link RunWithLooper}.
178      */
179     public void destroy() {
180         mQueueWrapper.release();
181         if (HOLD_MAIN_THREAD && mLooper == Looper.getMainLooper()) {
182             TestableInstrumentation.releaseMain();
183         }
184     }
185 
186     /**
187      * Sets a callback for all messages processed on this TestableLooper.
188      *
189      * @see {@link MessageHandler}
190      */
191     public void setMessageHandler(MessageHandler handler) {
192         mMessageHandler = handler;
193     }
194 
195     /**
196      * Parse num messages from the message queue.
197      *
198      * @param num Number of messages to parse
199      */
200     public int processMessages(int num) {
201         return processMessagesInternal(num, null);
202     }
203 
204     private int processMessagesInternal(int num, Runnable barrierRunnable) {
205         for (int i = 0; i < num; i++) {
206             if (!processSingleMessage(barrierRunnable)) {
207                 return i + 1;
208             }
209         }
210         return num;
211     }
212 
213     /**
214      * Process up to a certain number of messages, not blocking if the queue has less messages than
215      * that
216      * @param num the maximum number of messages to process
217      * @return the number of messages processed. This will be at most {@code num}.
218      */
219 
220     public int processMessagesNonBlocking(int num) {
221         final AtomicBoolean reachedBarrier = new AtomicBoolean(false);
222         Runnable barrierRunnable = () -> {
223             reachedBarrier.set(true);
224         };
225         mHandler.post(barrierRunnable);
226         waitForMessage(mQueueWrapper, mHandler, barrierRunnable);
227         try {
228             return processMessagesInternal(num, barrierRunnable) + (reachedBarrier.get() ? -1 : 0);
229         } finally {
230             mHandler.removeCallbacks(barrierRunnable);
231         }
232     }
233 
234     /**
235      * Process messages in the queue until no more are found.
236      */
237     public void processAllMessages() {
238         while (processQueuedMessages() != 0) ;
239     }
240 
241     public long peekWhen() {
242         if (isAtLeastBaklava()) {
243             return peekWhenBaklava();
244         } else {
245             return peekWhenLegacy();
246         }
247     }
248 
249     private long peekWhenBaklava() {
250         Long when = mQueueWrapper.peekWhen();
251         if (when != null) {
252             return when;
253         } else {
254             return 0;
255         }
256     }
257 
258     private long peekWhenLegacy() {
259         try {
260             Message msg = (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(mLooper.getQueue());
261             if (msg != null) {
262                 return msg.getWhen();
263             } else {
264                 return 0;
265             }
266         } catch (IllegalAccessException e) {
267             throw new RuntimeException("Access failed in TestableLooper: set - Message.when", e);
268         }
269     }
270 
271     public void moveTimeForward(long milliSeconds) {
272         if (isAtLeastBaklava()) {
273             moveTimeForwardBaklava(milliSeconds);
274         } else {
275             moveTimeForwardLegacy(milliSeconds);
276         }
277     }
278 
279     private void moveTimeForwardBaklava(long milliSeconds) {
280         // Drain all Messages from the queue.
281         Queue<Message> messages = new ArrayDeque<>();
282         while (true) {
283             Message message = mQueueWrapper.poll();
284             if (message == null) {
285                 break;
286             }
287 
288             // Adjust the Message's delivery time.
289             long newWhen = message.when - milliSeconds;
290             if (newWhen < 0) {
291                 newWhen = 0;
292             }
293             message.when = newWhen;
294             messages.add(message);
295         }
296 
297         // Repost all Messages back to the queue with a new time.
298         while (true) {
299             Message message = messages.poll();
300             if (message == null) {
301                 break;
302             }
303 
304             Runnable callback = message.getCallback();
305             Handler handler = message.getTarget();
306             long when = message.getWhen();
307 
308             // The Message cannot be re-enqueued because it is marked in use.
309             // Make a copy of the Message and recycle the original.
310             // This resets {@link Message#isInUse()} but retains all other content.
311             {
312                 Message newMessage = Message.obtain();
313                 newMessage.copyFrom(message);
314                 newMessage.setCallback(callback);
315                 mQueueWrapper.recycle(message);
316                 message = newMessage;
317             }
318 
319             // Send the Message back to its Handler to be re-enqueued.
320             handler.sendMessageAtTime(message, when);
321         }
322     }
323 
324     private void moveTimeForwardLegacy(long milliSeconds) {
325         try {
326             Message msg = (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(mLooper.getQueue());
327             while (msg != null) {
328                 long updatedWhen = msg.getWhen() - milliSeconds;
329                 if (updatedWhen < 0) {
330                     updatedWhen = 0;
331                 }
332                 MESSAGE_WHEN_FIELD.set(msg, updatedWhen);
333                 msg = (Message) MESSAGE_NEXT_FIELD.get(msg);
334             }
335         } catch (IllegalAccessException e) {
336             throw new RuntimeException("Access failed in TestableLooper: set - Message.when", e);
337         }
338     }
339 
340     private int processQueuedMessages() {
341         int count = 0;
342         Runnable barrierRunnable = () -> { };
343         mHandler.post(barrierRunnable);
344         waitForMessage(mQueueWrapper, mHandler, barrierRunnable);
345         while (processSingleMessage(barrierRunnable)) count++;
346         return count;
347     }
348 
349     private boolean processSingleMessage(Runnable barrierRunnable) {
350         try {
351             Message result = mQueueWrapper.next();
352             if (result != null) {
353                 // This is a break message.
354                 if (result.getCallback() == barrierRunnable) {
355                     mQueueWrapper.execute(result);
356                     mQueueWrapper.recycle(result);
357                     return false;
358                 }
359 
360                 if (mMessageHandler != null) {
361                     if (mMessageHandler.onMessageHandled(result)) {
362                         mQueueWrapper.execute(result);
363                         mQueueWrapper.recycle(result);
364                     } else {
365                         mQueueWrapper.recycle(result);
366                         // Message handler indicated it doesn't want us to continue.
367                         return false;
368                     }
369                 } else {
370                     mQueueWrapper.execute(result);
371                     mQueueWrapper.recycle(result);
372                 }
373             } else {
374                 // No messages, don't continue parsing
375                 return false;
376             }
377         } catch (Exception e) {
378             throw new RuntimeException(e);
379         }
380         return true;
381     }
382 
383     /**
384      * Runs an executable with myLooper set and processes all messages added.
385      */
386     public void runWithLooper(RunnableWithException runnable) throws Exception {
387         new Handler(getLooper()).post(() -> {
388             try {
389                 runnable.run();
390             } catch (Exception e) {
391                 throw new RuntimeException(e);
392             }
393         });
394         processAllMessages();
395     }
396 
397     public interface RunnableWithException {
398         void run() throws Exception;
399     }
400 
401     /**
402      * Annotation that tells the {@link AndroidTestingRunner} to create a TestableLooper and
403      * run this test/class on that thread. The {@link TestableLooper} can be acquired using
404      * {@link #get(Object)}.
405      */
406     @Retention(RetentionPolicy.RUNTIME)
407     @Target({ElementType.METHOD, ElementType.TYPE})
408     public @interface RunWithLooper {
409         boolean setAsMainLooper() default false;
410     }
411 
412     private static void waitForMessage(TestLooperManager queueWrapper, Handler handler,
413             Runnable execute) {
414         for (int i = 0; i < 10; i++) {
415             if (!queueWrapper.hasMessages(handler, null, execute)) {
416                 try {
417                     Thread.sleep(1);
418                 } catch (InterruptedException e) {
419                 }
420             }
421         }
422         if (!queueWrapper.hasMessages(handler, null, execute)) {
423             throw new RuntimeException("Message didn't queue...");
424         }
425     }
426 
427     private static TestLooperManager acquireLooperManager(Looper l) {
428         if (HOLD_MAIN_THREAD && l == Looper.getMainLooper()) {
429             TestableInstrumentation.acquireMain();
430         }
431         return InstrumentationRegistry.getInstrumentation().acquireLooperManager(l);
432     }
433 
434     private static final Map<Object, TestableLooperHolder> sLoopers = new ArrayMap<>();
435 
436     /**
437      * For use with {@link RunWithLooper}, used to get the TestableLooper that was
438      * automatically created for this test.
439      */
440     public static TestableLooper get(Object test) {
441         final TestableLooperHolder looperHolder = sLoopers.get(test);
442         return (looperHolder != null) ? looperHolder.mTestableLooper : null;
443     }
444 
445     public static void remove(Object test) {
446         sLoopers.remove(test);
447     }
448 
449     /**
450      * Holder object that contains {@link TestableLooper} so that its initialization can be
451      * deferred until a test case is actually run, instead of forcing it to be created at
452      * {@link FrameworkMethod} construction time.
453      *
454      * This deferral is important because some test environments may configure
455      * {@link Looper#getMainLooper()} as part of a {@code Rule} instead of assuming it's globally
456      * initialized and unconditionally available.
457      */
458     private static class TestableLooperHolder {
459         private final boolean mSetAsMain;
460         private final Object mTest;
461 
462         private TestableLooper mTestableLooper;
463         private Looper mLooper;
464         private Handler mHandler;
465         private HandlerThread mHandlerThread;
466 
467         public TestableLooperHolder(boolean setAsMain, Object test) {
468             mSetAsMain = setAsMain;
469             mTest = test;
470         }
471 
472         public void ensureInit() {
473             if (mLooper != null) return;
474             try {
475                 mLooper = mSetAsMain ? Looper.getMainLooper() : createLooper();
476                 mTestableLooper = new TestableLooper(mLooper, false);
477                 if (!mSetAsMain) {
478                     mTestableLooper.getLooper().getThread().setName(mTest.getClass().getName());
479                 }
480             } catch (Exception e) {
481                 throw new RuntimeException(e);
482             }
483             mHandler = new Handler(mLooper);
484         }
485 
486         private Looper createLooper() {
487             // TODO: Find way to share these.
488             mHandlerThread = new HandlerThread(TestableLooper.class.getSimpleName());
489             mHandlerThread.start();
490             return mHandlerThread.getLooper();
491         }
492     }
493 
494     static class LooperFrameworkMethod extends FrameworkMethod {
495         private TestableLooperHolder mLooperHolder;
496 
497         public LooperFrameworkMethod(FrameworkMethod base, TestableLooperHolder looperHolder) {
498             super(base.getMethod());
499             mLooperHolder = looperHolder;
500         }
501 
502         public static FrameworkMethod get(FrameworkMethod base, boolean setAsMain, Object test) {
503             TestableLooperHolder looperHolder = sLoopers.get(test);
504             if (looperHolder == null) {
505                 looperHolder = new TestableLooperHolder(setAsMain, test);
506                 sLoopers.put(test, looperHolder);
507             }
508             return new LooperFrameworkMethod(base, looperHolder);
509         }
510 
511         @Override
512         public Object invokeExplosively(Object target, Object... params) throws Throwable {
513             mLooperHolder.ensureInit();
514             if (Looper.myLooper() == mLooperHolder.mLooper) {
515                 // Already on the right thread from another statement, just execute then.
516                 return super.invokeExplosively(target, params);
517             }
518             boolean set = mLooperHolder.mTestableLooper.mQueueWrapper == null;
519             if (set) {
520                 mLooperHolder.mTestableLooper.mQueueWrapper = acquireLooperManager(
521                         mLooperHolder.mLooper);
522             }
523             try {
524                 Object[] ret = new Object[1];
525                 // Run the execution on the looper thread.
526                 Runnable execute = () -> {
527                     try {
528                         ret[0] = super.invokeExplosively(target, params);
529                     } catch (Throwable throwable) {
530                         throw new LooperException(throwable);
531                     }
532                 };
533                 Message m = Message.obtain(mLooperHolder.mHandler, execute);
534 
535                 // Dispatch our message.
536                 try {
537                     mLooperHolder.mTestableLooper.mQueueWrapper.execute(m);
538                 } catch (LooperException e) {
539                     throw e.getSource();
540                 } catch (RuntimeException re) {
541                     // If the TestLooperManager has to post, it will wrap what it throws in a
542                     // RuntimeException, make sure we grab the actual source.
543                     if (re.getCause() instanceof LooperException) {
544                         throw ((LooperException) re.getCause()).getSource();
545                     } else {
546                         throw re.getCause();
547                     }
548                 } finally {
549                     m.recycle();
550                 }
551                 return ret[0];
552             } finally {
553                 if (set) {
554                     mLooperHolder.mTestableLooper.mQueueWrapper.release();
555                     mLooperHolder.mTestableLooper.mQueueWrapper = null;
556                     if (HOLD_MAIN_THREAD && mLooperHolder.mLooper == Looper.getMainLooper()) {
557                         TestableInstrumentation.releaseMain();
558                     }
559                 }
560             }
561         }
562 
563         @Override
564         protected void finalize() throws Throwable {
565             super.finalize();
566             if (mLooperHolder.mHandlerThread != null) {
567                 mLooperHolder.mHandlerThread.quit();
568             }
569         }
570 
571         private static class LooperException extends RuntimeException {
572             private final Throwable mSource;
573 
574             public LooperException(Throwable t) {
575                 mSource = t;
576             }
577 
578             public Throwable getSource() {
579                 return mSource;
580             }
581         }
582     }
583 
584     /**
585      * Callback to control the execution of messages on the looper, when set with
586      * {@link #setMessageHandler(MessageHandler)} then {@link #onMessageHandled(Message)}
587      * will get called back for every message processed on the {@link TestableLooper}.
588      */
589     public interface MessageHandler {
590         /**
591          * Return true to have the message executed and delivered to target.
592          * Return false to not execute the message and stop executing messages.
593          */
594         boolean onMessageHandled(Message m);
595     }
596 }
597