• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.os.test;
18 
19 import static org.junit.Assert.assertTrue;
20 
21 import android.os.Build;
22 import android.os.Handler;
23 import android.os.HandlerExecutor;
24 import android.os.Looper;
25 import android.os.Message;
26 import android.os.MessageQueue;
27 import android.os.SystemClock;
28 import android.os.TestLooperManager;
29 import android.util.Log;
30 
31 import androidx.test.platform.app.InstrumentationRegistry;
32 
33 import java.lang.reflect.Constructor;
34 import java.lang.reflect.Field;
35 import java.lang.reflect.InvocationTargetException;
36 import java.lang.reflect.Method;
37 import java.util.ArrayDeque;
38 import java.util.Queue;
39 import java.util.concurrent.Executor;
40 
41 /**
42  * Creates a looper whose message queue can be manipulated This allows testing code that uses a
43  * looper to dispatch messages in a deterministic manner Creating a TestLooper will also install it
44  * as the looper for the current thread
45  *
46  * @deprecated Use {@link android.os.TestLooperManager} or {@link
47  *     org.robolectric.shadows.ShadowLooper} instead.
48  *     This class is not actively maintained.
49  *     Both of the recommended alternatives allow fine control of execution.
50  *     The Robolectric class also allows advancing time.
51  */
52 public class TestLooper {
53     private final Looper mLooper;
54     private final TestLooperManager mTestLooperManager;
55     private final Clock mClock;
56 
57     private static final Constructor<Looper> LOOPER_CONSTRUCTOR;
58     private static final Field THREAD_LOCAL_LOOPER_FIELD;
59     private static final Field MESSAGE_QUEUE_MESSAGES_FIELD;
60     private static final Field MESSAGE_NEXT_FIELD;
61     private static final Field MESSAGE_WHEN_FIELD;
62     private static final Method MESSAGE_MARK_IN_USE_METHOD;
63     private static final String TAG = "TestLooper";
64 
65     private AutoDispatchThread mAutoDispatchThread;
66 
67     /**
68      * Baklava introduces new {@link TestLooperManager} APIs that we can use instead of reflection.
69      */
isAtLeastBaklava()70     private static boolean isAtLeastBaklava() {
71         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA;
72     }
73 
74     static {
75         try {
76             LOOPER_CONSTRUCTOR = Looper.class.getDeclaredConstructor(Boolean.TYPE);
77             LOOPER_CONSTRUCTOR.setAccessible(true);
78             THREAD_LOCAL_LOOPER_FIELD = Looper.class.getDeclaredField("sThreadLocal");
79             THREAD_LOCAL_LOOPER_FIELD.setAccessible(true);
80 
81             if (isAtLeastBaklava()) {
82                 MESSAGE_QUEUE_MESSAGES_FIELD = null;
83                 MESSAGE_NEXT_FIELD = null;
84                 MESSAGE_WHEN_FIELD = null;
85                 MESSAGE_MARK_IN_USE_METHOD = null;
86             } else {
87                 MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages");
88                 MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true);
89                 MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next");
90                 MESSAGE_NEXT_FIELD.setAccessible(true);
91                 MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when");
92                 MESSAGE_WHEN_FIELD.setAccessible(true);
93                 MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse");
94                 MESSAGE_MARK_IN_USE_METHOD.setAccessible(true);
95             }
96         } catch (NoSuchFieldException | NoSuchMethodException e) {
97             throw new RuntimeException("Failed to initialize TestLooper", e);
98         }
99     }
100 
101     /**
102      * Creates a TestLooper and installs it as the looper for the current thread.
103      */
TestLooper()104     public TestLooper() {
105         this(SystemClock::uptimeMillis);
106     }
107 
108     /**
109      * Creates a TestLooper with a custom clock and installs it as the looper for the current
110      * thread.
111      *
112      * Messages are dispatched when their {@link Message#when} is before or at {@link
113      * Clock#uptimeMillis()}.
114      * Use a custom clock with care. When using an offsettable clock like {@link
115      * com.android.server.testutils.OffsettableClock} be sure not to double offset messages by
116      * offsetting the clock and calling {@link #moveTimeForward(long)}. Instead, offset the clock
117      * and call {@link #dispatchAll()}.
118      */
TestLooper(Clock clock)119     public TestLooper(Clock clock) {
120         try {
121             mLooper = LOOPER_CONSTRUCTOR.newInstance(false);
122 
123             ThreadLocal<Looper> threadLocalLooper =
124                     (ThreadLocal<Looper>) THREAD_LOCAL_LOOPER_FIELD.get(null);
125             threadLocalLooper.set(mLooper);
126         } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
127             throw new RuntimeException("Reflection error constructing or accessing looper", e);
128         }
129 
130         if (isAtLeastBaklava()) {
131             mTestLooperManager =
132                 InstrumentationRegistry.getInstrumentation().acquireLooperManager(mLooper);
133         } else {
134             mTestLooperManager = null;
135         }
136 
137         mClock = clock;
138     }
139 
getLooper()140     public Looper getLooper() {
141         return mLooper;
142     }
143 
getNewExecutor()144     public Executor getNewExecutor() {
145         return new HandlerExecutor(new Handler(getLooper()));
146     }
147 
getMessageLinkedListLegacy()148     private Message getMessageLinkedListLegacy() {
149         try {
150             MessageQueue queue = mLooper.getQueue();
151             return (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(queue);
152         } catch (IllegalAccessException e) {
153             throw new RuntimeException("Access failed in TestLooper: get - MessageQueue.mMessages",
154                 e);
155         }
156     }
157 
moveTimeForward(long milliSeconds)158     public void moveTimeForward(long milliSeconds) {
159         if (isAtLeastBaklava()) {
160             moveTimeForwardBaklava(milliSeconds);
161         } else {
162             moveTimeForwardLegacy(milliSeconds);
163         }
164     }
165 
moveTimeForwardBaklava(long milliSeconds)166     private void moveTimeForwardBaklava(long milliSeconds) {
167         // Drain all Messages from the queue.
168         Queue<Message> messages = new ArrayDeque<>();
169         while (true) {
170             Message message = mTestLooperManager.poll();
171             if (message == null) {
172                 break;
173             }
174             messages.add(message);
175         }
176 
177         // Repost all Messages back to the queue with a new time.
178         while (true) {
179             Message message = messages.poll();
180             if (message == null) {
181                 break;
182             }
183 
184             // Ugly trick to reset the Message's "in use" flag.
185             // This is needed because the Message cannot be re-enqueued if it's
186             // marked in use.
187             message.copyFrom(message);
188 
189             // Adjust the Message's delivery time.
190             long newWhen = message.getWhen() - milliSeconds;
191             if (newWhen < 0) {
192                 newWhen = 0;
193             }
194 
195             // Send the Message back to its Handler to be re-enqueued.
196             message.getTarget().sendMessageAtTime(message, newWhen);
197         }
198     }
199 
moveTimeForwardLegacy(long milliSeconds)200     private void moveTimeForwardLegacy(long milliSeconds) {
201         try {
202             Message msg = getMessageLinkedListLegacy();
203             while (msg != null) {
204                 long updatedWhen = msg.getWhen() - milliSeconds;
205                 if (updatedWhen < 0) {
206                     updatedWhen = 0;
207                 }
208                 MESSAGE_WHEN_FIELD.set(msg, updatedWhen);
209                 msg = (Message) MESSAGE_NEXT_FIELD.get(msg);
210             }
211         } catch (IllegalAccessException e) {
212             throw new RuntimeException("Access failed in TestLooper: set - Message.when", e);
213         }
214     }
215 
currentTime()216     private long currentTime() {
217         return mClock.uptimeMillis();
218     }
219 
messageQueueNextLegacy()220     private Message messageQueueNextLegacy() {
221         try {
222             long now = currentTime();
223 
224             Message prevMsg = null;
225             Message msg = getMessageLinkedListLegacy();
226             if (msg != null && msg.getTarget() == null) {
227                 // Stalled by a barrier. Find the next asynchronous message in
228                 // the queue.
229                 do {
230                     prevMsg = msg;
231                     msg = (Message) MESSAGE_NEXT_FIELD.get(msg);
232                 } while (msg != null && !msg.isAsynchronous());
233             }
234             if (msg != null) {
235                 if (now >= msg.getWhen()) {
236                     // Got a message.
237                     if (prevMsg != null) {
238                         MESSAGE_NEXT_FIELD.set(prevMsg, MESSAGE_NEXT_FIELD.get(msg));
239                     } else {
240                         MESSAGE_QUEUE_MESSAGES_FIELD.set(mLooper.getQueue(),
241                                 MESSAGE_NEXT_FIELD.get(msg));
242                     }
243                     MESSAGE_NEXT_FIELD.set(msg, null);
244                     MESSAGE_MARK_IN_USE_METHOD.invoke(msg);
245                     return msg;
246                 }
247             }
248         } catch (IllegalAccessException | InvocationTargetException e) {
249             throw new RuntimeException("Access failed in TestLooper", e);
250         }
251 
252         return null;
253     }
254 
255     /**
256      * @return true if there are pending messages in the message queue
257      */
isIdle()258     public boolean isIdle() {
259         if (isAtLeastBaklava()) {
260             return isIdleBaklava();
261         } else {
262             return isIdleLegacy();
263         }
264     }
265 
isIdleBaklava()266     private boolean isIdleBaklava() {
267         Long when = mTestLooperManager.peekWhen();
268         return when != null && currentTime() >= when;
269     }
270 
isIdleLegacy()271     private synchronized boolean isIdleLegacy() {
272         Message messageList = getMessageLinkedListLegacy();
273         return messageList != null && currentTime() >= messageList.getWhen();
274     }
275 
276     /**
277      * @return the next message in the Looper's message queue or null if there is none
278      */
nextMessage()279     public Message nextMessage() {
280         if (isAtLeastBaklava()) {
281             return nextMessageBaklava();
282         } else {
283             return nextMessageLegacy();
284         }
285     }
286 
nextMessageBaklava()287     private Message nextMessageBaklava() {
288         if (isIdle()) {
289             return mTestLooperManager.poll();
290         } else {
291             return null;
292         }
293     }
294 
nextMessageLegacy()295     private synchronized Message nextMessageLegacy() {
296         if (isIdle()) {
297             return messageQueueNextLegacy();
298         } else {
299             return null;
300         }
301     }
302 
303     /**
304      * Dispatch the next message in the queue
305      * Asserts that there is a message in the queue
306      */
dispatchNext()307     public void dispatchNext() {
308         if (isAtLeastBaklava()) {
309             dispatchNextBaklava();
310         } else {
311             dispatchNextLegacy();
312         }
313     }
314 
dispatchNextBaklava()315     private void dispatchNextBaklava() {
316         assertTrue(isIdle());
317         Message msg = mTestLooperManager.poll();
318         if (msg == null) {
319             return;
320         }
321         msg.getTarget().dispatchMessage(msg);
322     }
323 
dispatchNextLegacy()324     private synchronized void dispatchNextLegacy() {
325         assertTrue(isIdle());
326         Message msg = messageQueueNextLegacy();
327         if (msg == null) {
328             return;
329         }
330         msg.getTarget().dispatchMessage(msg);
331     }
332 
333     /**
334      * Dispatch all messages currently in the queue
335      * Will not fail if there are no messages pending
336      *
337      * @return the number of messages dispatched
338      */
dispatchAll()339     public synchronized int dispatchAll() {
340         int count = 0;
341         while (isIdle()) {
342             dispatchNext();
343             ++count;
344         }
345         return count;
346     }
347 
348     public interface Clock {
uptimeMillis()349         long uptimeMillis();
350     }
351 
352     /**
353      * Thread used to dispatch messages when the main thread is blocked waiting for a response.
354      */
355     private class AutoDispatchThread extends Thread {
356         private static final int MAX_LOOPS = 100;
357         private static final int LOOP_SLEEP_TIME_MS = 10;
358 
359         private RuntimeException mAutoDispatchException = null;
360 
361         /**
362          * Run method for the auto dispatch thread.
363          * The thread loops a maximum of MAX_LOOPS times with a 10ms sleep between loops.
364          * The thread continues looping and attempting to dispatch all messages until
365          * {@link #stopAutoDispatch()} has been invoked.
366          */
367         @Override
run()368         public void run() {
369             int dispatchCount = 0;
370             for (int i = 0; i < MAX_LOOPS; i++) {
371                 try {
372                     dispatchCount += dispatchAll();
373                 } catch (RuntimeException e) {
374                     mAutoDispatchException = e;
375                     return;
376                 }
377                 Log.d(TAG, "dispatched " + dispatchCount + " messages");
378                 try {
379                     Thread.sleep(LOOP_SLEEP_TIME_MS);
380                 } catch (InterruptedException e) {
381                     if (dispatchCount == 0) {
382                         Log.e(TAG, "stopAutoDispatch called before any messages were dispatched.");
383                         mAutoDispatchException = new IllegalStateException(
384                                 "stopAutoDispatch called before any messages were dispatched.");
385                     }
386                     return;
387                 }
388             }
389             if (dispatchCount == 0) {
390                 Log.e(TAG, "AutoDispatchThread did not dispatch any messages.");
391                 mAutoDispatchException = new IllegalStateException(
392                         "TestLooper did not dispatch any messages before exiting.");
393             }
394         }
395 
396         /**
397          * Method allowing the TestLooper to pass any exceptions thrown by the thread to be passed
398          * to the main thread.
399          *
400          * @return RuntimeException Exception created by stopping without dispatching a message
401          */
getException()402         public RuntimeException getException() {
403             return mAutoDispatchException;
404         }
405     }
406 
407     /**
408      * Create and start a new AutoDispatchThread if one is not already running.
409      */
startAutoDispatch()410     public void startAutoDispatch() {
411         if (mAutoDispatchThread != null) {
412             throw new IllegalStateException(
413                     "startAutoDispatch called with the AutoDispatchThread already running.");
414         }
415         mAutoDispatchThread = new AutoDispatchThread();
416         mAutoDispatchThread.start();
417     }
418 
419     /**
420      * If an AutoDispatchThread is currently running, stop and clean up.
421      */
stopAutoDispatch()422     public void stopAutoDispatch() {
423         if (mAutoDispatchThread != null) {
424             if (mAutoDispatchThread.isAlive()) {
425                 mAutoDispatchThread.interrupt();
426             }
427             try {
428                 mAutoDispatchThread.join();
429             } catch (InterruptedException e) {
430                 // Catch exception from join.
431             }
432 
433             RuntimeException e = mAutoDispatchThread.getException();
434             mAutoDispatchThread = null;
435             if (e != null) {
436                 throw e;
437             }
438         } else {
439             // stopAutoDispatch was called when startAutoDispatch has not created a new thread.
440             throw new IllegalStateException(
441                     "stopAutoDispatch called without startAutoDispatch.");
442         }
443     }
444 
445     /**
446      * If an AutoDispatchThread is currently running, stop and clean up.
447      * This method ignores exceptions raised for indicating that no messages were dispatched.
448      */
stopAutoDispatchAndIgnoreExceptions()449     public void stopAutoDispatchAndIgnoreExceptions() {
450         try {
451             stopAutoDispatch();
452         } catch (IllegalStateException e) {
453             Log.e(TAG, "stopAutoDispatch", e);
454         }
455 
456     }
457 }
458