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