• 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.time.Duration;
11 import java.util.ArrayList;
12 import java.util.Collection;
13 import java.util.Collections;
14 import java.util.List;
15 import java.util.Map;
16 import java.util.WeakHashMap;
17 import java.util.concurrent.TimeUnit;
18 import org.robolectric.RoboSettings;
19 import org.robolectric.RuntimeEnvironment;
20 import org.robolectric.annotation.Implementation;
21 import org.robolectric.annotation.Implements;
22 import org.robolectric.annotation.LooperMode;
23 import org.robolectric.annotation.RealObject;
24 import org.robolectric.annotation.Resetter;
25 import org.robolectric.config.ConfigurationRegistry;
26 import org.robolectric.shadow.api.Shadow;
27 import org.robolectric.util.Scheduler;
28 
29 /**
30  * The shadow Looper implementation for {@link LooperMode.Mode.LEGACY}.
31  *
32  * <p>Robolectric enqueues posted {@link Runnable}s to be run (on this thread) later. {@code
33  * Runnable}s that are scheduled to run immediately can be triggered by calling {@link #idle()}.
34  *
35  * @see ShadowMessageQueue
36  */
37 @Implements(value = Looper.class, isInAndroidSdk = false)
38 @SuppressWarnings("SynchronizeOnNonFinalField")
39 public class ShadowLegacyLooper extends ShadowLooper {
40 
41   // Replaced SoftThreadLocal with a WeakHashMap, because ThreadLocal make it impossible to access
42   // their contents from other threads, but we need to be able to access the loopers for all
43   // threads so that we can shut them down when resetThreadLoopers()
44   // is called. This also allows us to implement the useful getLooperForThread() method.
45   // Note that the main looper is handled differently and is not put in this hash, because we need
46   // to be able to "switch" the thread that the main looper is associated with.
47   private static Map<Thread, Looper> loopingLoopers =
48       Collections.synchronizedMap(new WeakHashMap<Thread, Looper>());
49 
50   private static Looper mainLooper;
51 
52   private static Scheduler backgroundScheduler;
53 
54   private @RealObject Looper realObject;
55 
56   boolean quit;
57 
58   @Resetter
resetThreadLoopers()59   public static synchronized void resetThreadLoopers() {
60     // do not use looperMode() here, because its cached value might already have been reset
61     if (ConfigurationRegistry.get(LooperMode.Mode.class) == LooperMode.Mode.PAUSED) {
62       // ignore if realistic looper
63       return;
64     }
65     synchronized (loopingLoopers) {
66       for (Looper looper : loopingLoopers.values()) {
67         synchronized (looper) {
68           if (!shadowOf(looper).quit) {
69             looper.quit();
70           } else {
71             // Reset the schedulers of all loopers. This prevents un-run tasks queued up in static
72             // background handlers from leaking to subsequent tests.
73             shadowOf(looper).getScheduler().reset();
74             shadowOf(looper.getQueue()).reset();
75           }
76         }
77       }
78     }
79     // Because resetStaticState() is called by AndroidTestEnvironment on startup before
80     // prepareMainLooper() is called, this might be null on that occasion.
81     if (mainLooper != null) {
82       shadowOf(mainLooper).reset();
83     }
84   }
85 
getBackgroundThreadScheduler()86   static synchronized Scheduler getBackgroundThreadScheduler() {
87     return backgroundScheduler;
88   }
89 
90   /** Internal API to initialize background thread scheduler from AndroidTestEnvironment. */
internalInitializeBackgroundThreadScheduler()91   public static void internalInitializeBackgroundThreadScheduler() {
92     backgroundScheduler =
93         RoboSettings.isUseGlobalScheduler()
94             ? RuntimeEnvironment.getMasterScheduler()
95             : new Scheduler();
96   }
97 
98   @Implementation
__constructor__(boolean quitAllowed)99   protected void __constructor__(boolean quitAllowed) {
100     invokeConstructor(Looper.class, realObject, from(boolean.class, quitAllowed));
101     if (isMainThread()) {
102       mainLooper = realObject;
103     } else {
104       loopingLoopers.put(Thread.currentThread(), realObject);
105     }
106     resetScheduler();
107   }
108 
109   @Implementation
getMainLooper()110   protected static Looper getMainLooper() {
111     return mainLooper;
112   }
113 
114   @Implementation
myLooper()115   protected static Looper myLooper() {
116     return getLooperForThread(Thread.currentThread());
117   }
118 
119   @Implementation
loop()120   protected static void loop() {
121     shadowOf(Looper.myLooper()).doLoop();
122   }
123 
doLoop()124   private void doLoop() {
125     if (realObject != Looper.getMainLooper()) {
126       synchronized (realObject) {
127         while (!quit) {
128           try {
129             realObject.wait();
130           } catch (InterruptedException ignore) {
131           }
132         }
133       }
134     }
135   }
136 
137   @Implementation
quit()138   protected void quit() {
139     if (realObject == Looper.getMainLooper()) {
140       throw new RuntimeException("Main thread not allowed to quit");
141     }
142     quitUnchecked();
143   }
144 
145   @Implementation(minSdk = JELLY_BEAN_MR2)
quitSafely()146   protected void quitSafely() {
147     quit();
148   }
149 
150   @Override
quitUnchecked()151   public void quitUnchecked() {
152     synchronized (realObject) {
153       quit = true;
154       realObject.notifyAll();
155       getScheduler().reset();
156       shadowOf(realObject.getQueue()).reset();
157     }
158   }
159 
160   @Override
hasQuit()161   public boolean hasQuit() {
162     synchronized (realObject) {
163       return quit;
164     }
165   }
166 
getLooperForThread(Thread thread)167   public static Looper getLooperForThread(Thread thread) {
168     return isMainThread(thread) ? mainLooper : loopingLoopers.get(thread);
169   }
170 
171   /** Return loopers for all threads including main thread. */
getLoopers()172   protected static Collection<Looper> getLoopers() {
173     List<Looper> loopers = new ArrayList<>(loopingLoopers.values());
174     loopers.add(mainLooper);
175     return Collections.unmodifiableCollection(loopers);
176   }
177 
178   @Override
idle()179   public void idle() {
180     idle(0, TimeUnit.MILLISECONDS);
181   }
182 
183   @Override
idleFor(long time, TimeUnit timeUnit)184   public void idleFor(long time, TimeUnit timeUnit) {
185     getScheduler().advanceBy(time, timeUnit);
186   }
187 
188   @Override
isIdle()189   public boolean isIdle() {
190     return !getScheduler().areAnyRunnable();
191   }
192 
193   @Override
idleIfPaused()194   public void idleIfPaused() {
195     // ignore
196   }
197 
198   @Override
idleConstantly(boolean shouldIdleConstantly)199   public void idleConstantly(boolean shouldIdleConstantly) {
200     getScheduler().idleConstantly(shouldIdleConstantly);
201   }
202 
203   @Override
runToEndOfTasks()204   public void runToEndOfTasks() {
205     getScheduler().advanceToLastPostedRunnable();
206   }
207 
208   @Override
runToNextTask()209   public void runToNextTask() {
210     getScheduler().advanceToNextPostedRunnable();
211   }
212 
213   @Override
runOneTask()214   public void runOneTask() {
215     getScheduler().runOneTask();
216   }
217 
218   /**
219    * Enqueue a task to be run later.
220    *
221    * @param runnable the task to be run
222    * @param delayMillis how many milliseconds into the (virtual) future to run it
223    * @return true if the runnable is enqueued
224    * @see android.os.Handler#postDelayed(Runnable,long)
225    * @deprecated Use a {@link android.os.Handler} instance to post to a looper.
226    */
227   @Override
228   @Deprecated
post(Runnable runnable, long delayMillis)229   public boolean post(Runnable runnable, long delayMillis) {
230     if (!quit) {
231       getScheduler().postDelayed(runnable, delayMillis, TimeUnit.MILLISECONDS);
232       return true;
233     } else {
234       return false;
235     }
236   }
237 
238   /**
239    * Enqueue a task to be run ahead of all other delayed tasks.
240    *
241    * @param runnable the task to be run
242    * @return true if the runnable is enqueued
243    * @see android.os.Handler#postAtFrontOfQueue(Runnable)
244    * @deprecated Use a {@link android.os.Handler} instance to post to a looper.
245    */
246   @Override
247   @Deprecated
postAtFrontOfQueue(Runnable runnable)248   public boolean postAtFrontOfQueue(Runnable runnable) {
249     if (!quit) {
250       getScheduler().postAtFrontOfQueue(runnable);
251       return true;
252     } else {
253       return false;
254     }
255   }
256 
257   @Override
pause()258   public void pause() {
259     getScheduler().pause();
260   }
261 
262   @Override
getNextScheduledTaskTime()263   public Duration getNextScheduledTaskTime() {
264     return getScheduler().getNextScheduledTaskTime();
265   }
266 
267   @Override
getLastScheduledTaskTime()268   public Duration getLastScheduledTaskTime() {
269     return getScheduler().getLastScheduledTaskTime();
270   }
271 
272   @Override
unPause()273   public void unPause() {
274     getScheduler().unPause();
275   }
276 
277   @Override
isPaused()278   public boolean isPaused() {
279     return getScheduler().isPaused();
280   }
281 
282   @Override
setPaused(boolean shouldPause)283   public boolean setPaused(boolean shouldPause) {
284     boolean wasPaused = isPaused();
285     if (shouldPause) {
286       pause();
287     } else {
288       unPause();
289     }
290     return wasPaused;
291   }
292 
293   @Override
resetScheduler()294   public void resetScheduler() {
295     ShadowMessageQueue shadowMessageQueue = shadowOf(realObject.getQueue());
296     if (realObject == Looper.getMainLooper() || RoboSettings.isUseGlobalScheduler()) {
297       shadowMessageQueue.setScheduler(RuntimeEnvironment.getMasterScheduler());
298     } else {
299       shadowMessageQueue.setScheduler(new Scheduler());
300     }
301   }
302 
303   /** Causes all enqueued tasks to be discarded, and pause state to be reset */
304   @Override
reset()305   public void reset() {
306     shadowOf(realObject.getQueue()).reset();
307     resetScheduler();
308 
309     quit = false;
310   }
311 
312   /**
313    * Returns the {@link org.robolectric.util.Scheduler} that is being used to manage the enqueued
314    * tasks. This scheduler is managed by the Looper's associated queue.
315    *
316    * @return the {@link org.robolectric.util.Scheduler} that is being used to manage the enqueued
317    *     tasks.
318    */
319   @Override
getScheduler()320   public Scheduler getScheduler() {
321     return shadowOf(realObject.getQueue()).getScheduler();
322   }
323 
324   @Override
runPaused(Runnable r)325   public void runPaused(Runnable r) {
326     boolean wasPaused = setPaused(true);
327     try {
328       r.run();
329     } finally {
330       if (!wasPaused) unPause();
331     }
332   }
333 
shadowOf(Looper looper)334   private static ShadowLegacyLooper shadowOf(Looper looper) {
335     return Shadow.extract(looper);
336   }
337 
shadowOf(MessageQueue mq)338   private static ShadowMessageQueue shadowOf(MessageQueue mq) {
339     return Shadow.extract(mq);
340   }
341 }
342