• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.robolectric.shadows;
2 
3 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
4 import static org.robolectric.RuntimeEnvironment.isMainThread;
5 import static org.robolectric.shadow.api.Shadow.invokeConstructor;
6 import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
7 
8 import android.os.Looper;
9 import android.os.MessageQueue;
10 import java.util.Collections;
11 import java.util.Map;
12 import java.util.WeakHashMap;
13 import java.util.concurrent.TimeUnit;
14 import org.robolectric.RoboSettings;
15 import org.robolectric.RuntimeEnvironment;
16 import org.robolectric.annotation.Implementation;
17 import org.robolectric.annotation.Implements;
18 import org.robolectric.annotation.RealObject;
19 import org.robolectric.annotation.Resetter;
20 import org.robolectric.shadow.api.Shadow;
21 import org.robolectric.util.Scheduler;
22 
23 /**
24  * Robolectric enqueues posted {@link Runnable}s to be run
25  * (on this thread) later. {@code Runnable}s that are scheduled to run immediately can be
26  * triggered by calling {@link #idle()}.
27  *
28  * @see ShadowMessageQueue
29  */
30 @Implements(Looper.class)
31 @SuppressWarnings("SynchronizeOnNonFinalField")
32 public class ShadowLooper {
33 
34   // Replaced SoftThreadLocal with a WeakHashMap, because ThreadLocal make it impossible to access their contents from other
35   // threads, but we need to be able to access the loopers for all threads so that we can shut them down when resetThreadLoopers()
36   // is called. This also allows us to implement the useful getLooperForThread() method.
37   // Note that the main looper is handled differently and is not put in this hash, because we need to be able to
38   // "switch" the thread that the main looper is associated with.
39   private static Map<Thread, Looper> loopingLoopers = Collections.synchronizedMap(new WeakHashMap<Thread, Looper>());
40 
41   private static Looper mainLooper;
42 
43   private @RealObject Looper realObject;
44 
45   boolean quit;
46 
47   @Resetter
resetThreadLoopers()48   public static synchronized void resetThreadLoopers() {
49     // Blech. We need to keep the main looper because somebody might refer to it in a static
50     // field. The other loopers need to be wrapped in WeakReferences so that they are not prevented from
51     // being garbage collected.
52     if (!isMainThread()) {
53       throw new IllegalStateException("you should only be calling this from the main thread!");
54     }
55     synchronized (loopingLoopers) {
56       for (Looper looper : loopingLoopers.values()) {
57         synchronized (looper) {
58           if (!shadowOf(looper).quit) {
59             looper.quit();
60           } else {
61             // Reset the schedulers of all loopers. This prevents un-run tasks queued up in static
62             // background handlers from leaking to subsequent tests.
63             shadowOf(looper).getScheduler().reset();
64             shadowOf(looper.getQueue()).reset();
65           }
66         }
67       }
68     }
69     // Because resetStaticState() is called by ParallelUniverse on startup before prepareMainLooper() is
70     // called, this might be null on that occasion.
71     if (mainLooper != null) {
72       shadowOf(mainLooper).reset();
73     }
74   }
75 
76   @Implementation
__constructor__(boolean quitAllowed)77   protected void __constructor__(boolean quitAllowed) {
78     invokeConstructor(Looper.class, realObject, from(boolean.class, quitAllowed));
79     if (isMainThread()) {
80       mainLooper = realObject;
81     } else {
82       loopingLoopers.put(Thread.currentThread(), realObject);
83     }
84     resetScheduler();
85   }
86 
87   @Implementation
getMainLooper()88   protected static Looper getMainLooper() {
89     return mainLooper;
90   }
91 
92   @Implementation
myLooper()93   protected static Looper myLooper() {
94     return getLooperForThread(Thread.currentThread());
95   }
96 
97   @Implementation
loop()98   protected static void loop() {
99     shadowOf(Looper.myLooper()).doLoop();
100   }
101 
doLoop()102   private void doLoop() {
103     if (realObject != Looper.getMainLooper()) {
104       synchronized (realObject) {
105         while (!quit) {
106           try {
107             realObject.wait();
108           } catch (InterruptedException ignore) {
109           }
110         }
111       }
112     }
113   }
114 
115   @Implementation
quit()116   protected void quit() {
117     if (realObject == Looper.getMainLooper()) throw new RuntimeException("Main thread not allowed to quit");
118     quitUnchecked();
119   }
120 
121   @Implementation(minSdk = JELLY_BEAN_MR2)
quitSafely()122   protected void quitSafely() {
123     quit();
124   }
125 
quitUnchecked()126   public void quitUnchecked() {
127     synchronized (realObject) {
128       quit = true;
129       realObject.notifyAll();
130       getScheduler().reset();
131       shadowOf(realObject.getQueue()).reset();
132     }
133   }
134 
hasQuit()135   public boolean hasQuit() {
136     synchronized (realObject) {
137       return quit;
138     }
139   }
140 
141   /** @deprecated Use `shadowOf({@link Looper#getMainLooper()})` instead. */
142   @Deprecated
getShadowMainLooper()143   public static ShadowLooper getShadowMainLooper() {
144     return shadowOf(Looper.getMainLooper());
145   }
146 
getLooperForThread(Thread thread)147   public static Looper getLooperForThread(Thread thread) {
148     return isMainThread(thread) ? mainLooper : loopingLoopers.get(thread);
149   }
150 
151   /**
152    * Pauses execution of tasks posted to the ShadowLooper. This means that during tests, tasks sent
153    * to the looper will not execute immediately, but will be queued in a way that is similar to how
154    * a real looper works. These queued tasks must be executed explicitly by calling {@link
155    * #runToEndOftasks} or a similar method, otherwise they will not run at all before your test
156    * ends.
157    *
158    * @param looper the looper to pause
159    */
pauseLooper(Looper looper)160   public static void pauseLooper(Looper looper) {
161     shadowOf(looper).pause();
162   }
163 
164   /**
165    * Puts the shadow looper in an "unpaused" state (this is the default state). This means that
166    * during tests, tasks sent to the looper will execute inline, immediately, on the calling (main)
167    * thread instead of being queued, in a way similar to how Guava's "DirectExecutorService" works.
168    * This is likely not to be what you want: it will cause code to be potentially executed in a
169    * different order than how it would execute on the device, and if you are using certain Android
170    * APIs (such as view animations) that are non-reentrant, they may not work at all or do
171    * unpredictable things. For more information, see <a
172    * href="https://github.com/robolectric/robolectric/issues/3369">this discussion</a>.
173    *
174    * @param looper the looper to pause
175    */
unPauseLooper(Looper looper)176   public static void unPauseLooper(Looper looper) {
177     shadowOf(looper).unPause();
178   }
179 
180   /**
181    * Puts the main ShadowLooper in an "paused" state.
182    *
183    * @see #pauseLooper
184    */
pauseMainLooper()185   public static void pauseMainLooper() {
186     getShadowMainLooper().pause();
187   }
188 
189   /**
190    * Puts the main ShadowLooper in an "unpaused" state.
191    *
192    * @see #unPauseLooper
193    */
unPauseMainLooper()194   public static void unPauseMainLooper() {
195     getShadowMainLooper().unPause();
196   }
197 
idleMainLooper()198   public static void idleMainLooper() {
199     getShadowMainLooper().idle();
200   }
201 
202   /** @deprecated Use {@link #idleMainLooper(long, TimeUnit)}. */
203   @Deprecated
idleMainLooper(long interval)204   public static void idleMainLooper(long interval) {
205     idleMainLooper(interval, TimeUnit.MILLISECONDS);
206   }
207 
idleMainLooper(long amount, TimeUnit unit)208   public static void idleMainLooper(long amount, TimeUnit unit) {
209     getShadowMainLooper().idle(amount, unit);
210   }
211 
idleMainLooperConstantly(boolean shouldIdleConstantly)212   public static void idleMainLooperConstantly(boolean shouldIdleConstantly) {
213     getShadowMainLooper().idleConstantly(shouldIdleConstantly);
214   }
215 
runMainLooperOneTask()216   public static void runMainLooperOneTask() {
217     getShadowMainLooper().runOneTask();
218   }
219 
runMainLooperToNextTask()220   public static void runMainLooperToNextTask() {
221     getShadowMainLooper().runToNextTask();
222   }
223 
224   /**
225    * Runs any immediately runnable tasks previously queued on the UI thread,
226    * e.g. by {@link android.app.Activity#runOnUiThread(Runnable)} or {@link android.os.AsyncTask#onPostExecute(Object)}.
227    *
228    * **Note:** calling this method does not pause or un-pause the scheduler.
229    *
230    * @see #runUiThreadTasksIncludingDelayedTasks
231    */
runUiThreadTasks()232   public static void runUiThreadTasks() {
233     getShadowMainLooper().idle();
234   }
235 
236   /**
237    * Runs all runnable tasks (pending and future) that have been queued on the UI thread. Such tasks may be queued by
238    * e.g. {@link android.app.Activity#runOnUiThread(Runnable)} or {@link android.os.AsyncTask#onPostExecute(Object)}.
239    *
240    * **Note:** calling this method does not pause or un-pause the scheduler, however the clock is advanced as
241    * future tasks are run.
242    *
243    * @see #runUiThreadTasks
244    */
runUiThreadTasksIncludingDelayedTasks()245   public static void runUiThreadTasksIncludingDelayedTasks() {
246     getShadowMainLooper().runToEndOfTasks();
247   }
248 
249   /**
250    * Causes {@link Runnable}s that have been scheduled to run immediately to actually run. Does not advance the
251    * scheduler's clock;
252    */
idle()253   public void idle() {
254     idle(0, TimeUnit.MILLISECONDS);
255   }
256 
257   /**
258    * Causes {@link Runnable}s that have been scheduled to run within the next {@code intervalMillis} milliseconds to
259    * run while advancing the scheduler's clock.
260    *
261    * @deprecated Use {@link #idle(long, TimeUnit)}.
262    */
263   @Deprecated
idle(long intervalMillis)264   public void idle(long intervalMillis) {
265     idle(intervalMillis, TimeUnit.MILLISECONDS);
266   }
267 
268   /**
269    * Causes {@link Runnable}s that have been scheduled to run within the next specified amount of time to run while
270    * advancing the scheduler's clock.
271    */
idle(long amount, TimeUnit unit)272   public void idle(long amount, TimeUnit unit) {
273     getScheduler().advanceBy(amount, unit);
274   }
275 
idleConstantly(boolean shouldIdleConstantly)276   public void idleConstantly(boolean shouldIdleConstantly) {
277     getScheduler().idleConstantly(shouldIdleConstantly);
278   }
279 
280   /**
281    * Causes all of the {@link Runnable}s that have been scheduled to run while advancing the scheduler's clock to the
282    * start time of the last scheduled {@link Runnable}.
283    */
runToEndOfTasks()284   public void runToEndOfTasks() {
285     getScheduler().advanceToLastPostedRunnable();
286   }
287 
288   /**
289    * Causes the next {@link Runnable}(s) that have been scheduled to run while advancing the scheduler's clock to its
290    * start time. If more than one {@link Runnable} is scheduled to run at this time then they will all be run.
291    */
runToNextTask()292   public void runToNextTask() {
293     getScheduler().advanceToNextPostedRunnable();
294   }
295 
296   /**
297    * Causes only one of the next {@link Runnable}s that have been scheduled to run while advancing the scheduler's
298    * clock to its start time. Only one {@link Runnable} will run even if more than one has ben scheduled to run at the
299    * same time.
300    */
runOneTask()301   public void runOneTask() {
302     getScheduler().runOneTask();
303   }
304 
305   /**
306    * Enqueue a task to be run later.
307    *
308    * @param runnable    the task to be run
309    * @param delayMillis how many milliseconds into the (virtual) future to run it
310    * @return true if the runnable is enqueued
311    * @see android.os.Handler#postDelayed(Runnable,long)
312    * @deprecated Use a {@link android.os.Handler} instance to post to a looper.
313    */
314   @Deprecated
post(Runnable runnable, long delayMillis)315   public boolean post(Runnable runnable, long delayMillis) {
316     if (!quit) {
317       getScheduler().postDelayed(runnable, delayMillis, TimeUnit.MILLISECONDS);
318       return true;
319     } else {
320       return false;
321     }
322   }
323 
324   /**
325    * Enqueue a task to be run ahead of all other delayed tasks.
326    *
327    * @param runnable    the task to be run
328    * @return true if the runnable is enqueued
329    * @see android.os.Handler#postAtFrontOfQueue(Runnable)
330    * @deprecated Use a {@link android.os.Handler} instance to post to a looper.
331    */
332   @Deprecated
postAtFrontOfQueue(Runnable runnable)333   public boolean postAtFrontOfQueue(Runnable runnable) {
334     if (!quit) {
335       getScheduler().postAtFrontOfQueue(runnable);
336       return true;
337     } else {
338       return false;
339     }
340   }
341 
pause()342   public void pause() {
343     getScheduler().pause();
344   }
345 
unPause()346   public void unPause() {
347     getScheduler().unPause();
348   }
349 
isPaused()350   public boolean isPaused() {
351     return getScheduler().isPaused();
352   }
353 
setPaused(boolean shouldPause)354   public boolean setPaused(boolean shouldPause) {
355     boolean wasPaused = isPaused();
356     if (shouldPause) {
357       pause();
358     } else {
359       unPause();
360     }
361     return wasPaused;
362   }
363 
resetScheduler()364   public void resetScheduler() {
365     ShadowMessageQueue shadowMessageQueue = shadowOf(realObject.getQueue());
366     if (realObject == Looper.getMainLooper() || RoboSettings.isUseGlobalScheduler()) {
367       shadowMessageQueue.setScheduler(RuntimeEnvironment.getMasterScheduler());
368     } else {
369       shadowMessageQueue.setScheduler(new Scheduler());
370     }
371   }
372 
373   /**
374    * Causes all enqueued tasks to be discarded, and pause state to be reset
375    */
reset()376   public void reset() {
377     shadowOf(realObject.getQueue()).reset();
378     resetScheduler();
379 
380     quit = false;
381   }
382 
383   /**
384    * Returns the {@link org.robolectric.util.Scheduler} that is being used to manage the enqueued tasks.
385    * This scheduler is managed by the Looper's associated queue.
386    *
387    * @return the {@link org.robolectric.util.Scheduler} that is being used to manage the enqueued tasks.
388    */
getScheduler()389   public Scheduler getScheduler() {
390     return shadowOf(realObject.getQueue()).getScheduler();
391   }
392 
runPaused(Runnable r)393   public void runPaused(Runnable r) {
394     boolean wasPaused = setPaused(true);
395     try {
396       r.run();
397     } finally {
398       if (!wasPaused) unPause();
399     }
400   }
401 
shadowOf(Looper looper)402   private static ShadowLooper shadowOf(Looper looper) {
403     return Shadow.extract(looper);
404   }
405 
shadowOf(MessageQueue mq)406   private static ShadowMessageQueue shadowOf(MessageQueue mq) {
407     return Shadow.extract(mq);
408   }
409 }
410