xref: /aosp_15_r20/external/robolectric/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLooper.java (revision e6ba16074e6af37d123cb567d575f496bf0a58ee)
1 package org.robolectric.shadows;
2 
3 import static android.os.Looper.getMainLooper;
4 import static java.util.concurrent.TimeUnit.MILLISECONDS;
5 import static org.robolectric.annotation.LooperMode.Mode.LEGACY;
6 
7 import android.os.Looper;
8 import com.google.errorprone.annotations.InlineMe;
9 import com.google.errorprone.annotations.concurrent.GuardedBy;
10 import java.time.Duration;
11 import java.util.Collection;
12 import java.util.concurrent.TimeUnit;
13 import org.robolectric.annotation.Implements;
14 import org.robolectric.annotation.LooperMode;
15 import org.robolectric.annotation.Resetter;
16 import org.robolectric.config.ConfigurationRegistry;
17 import org.robolectric.shadow.api.Shadow;
18 import org.robolectric.util.Scheduler;
19 
20 /**
21  * The base shadow API class for controlling Loopers.
22  *
23  * <p>It will delegate calls to the appropriate shadow based on the current LooperMode.
24  */
25 @Implements(value = Looper.class, shadowPicker = ShadowLooper.Picker.class)
26 public abstract class ShadowLooper {
27 
28   // cache for looperMode(), since this can be an expensive call.
29   @GuardedBy("looperModeLock")
30   private static LooperMode.Mode cachedLooperMode = null;
31 
32   private static final Object looperModeLock = new Object();
33 
assertLooperMode(LooperMode.Mode expectedMode)34   public static void assertLooperMode(LooperMode.Mode expectedMode) {
35     if (looperMode() != expectedMode) {
36       throw new IllegalStateException("this action is not supported in " + looperMode() + " mode.");
37     }
38   }
39 
shadowLooper(Looper looper)40   private static ShadowLooper shadowLooper(Looper looper) {
41     return Shadow.extract(looper);
42   }
43 
44   /**
45    * @deprecated Use {@code shadowOf({@link Looper#getMainLooper()})} instead.
46    */
47   @Deprecated
getShadowMainLooper()48   public static ShadowLooper getShadowMainLooper() {
49     return shadowLooper(getMainLooper());
50   }
51 
52   // TODO: should probably remove this
shadowMainLooper()53   public static ShadowLooper shadowMainLooper() {
54     return shadowLooper(getMainLooper());
55   }
56 
getLooperForThread(Thread thread)57   public static Looper getLooperForThread(Thread thread) {
58     if (looperMode() == LEGACY) {
59       return ShadowLegacyLooper.getLooperForThread(thread);
60     }
61     throw new UnsupportedOperationException(
62         "this action is not supported in " + looperMode() + " mode.");
63   }
64 
65   /** Return all created loopers. */
getAllLoopers()66   public static Collection<Looper> getAllLoopers() {
67     if (looperMode() == LEGACY) {
68       return ShadowLegacyLooper.getLoopers();
69     } else {
70       return ShadowPausedLooper.getLoopers();
71     }
72   }
73 
74   /** Should not be called directly - Robolectric internal use only. */
resetThreadLoopers()75   public static void resetThreadLoopers() {
76     if (looperMode() == LEGACY) {
77       ShadowLegacyLooper.resetThreadLoopers();
78       return;
79     }
80     throw new UnsupportedOperationException(
81         "this action is not supported in " + looperMode() + " mode.");
82   }
83 
84   /** Return the current {@link LooperMode}. */
looperMode()85   public static LooperMode.Mode looperMode() {
86     synchronized (looperModeLock) {
87       if (cachedLooperMode == null) {
88         cachedLooperMode = ConfigurationRegistry.get(LooperMode.Mode.class);
89       }
90       return cachedLooperMode;
91     }
92   }
93 
94   @Resetter
clearLooperMode()95   public static synchronized void clearLooperMode() {
96     synchronized (looperModeLock) {
97       cachedLooperMode = null;
98     }
99   }
100 
101   /**
102    * Pauses execution of tasks posted to the ShadowLegacyLooper. This means that during tests, tasks
103    * sent to the looper will not execute immediately, but will be queued in a way that is similar to
104    * how a real looper works. These queued tasks must be executed explicitly by calling {@link
105    * #runToEndOftasks} or a similar method, otherwise they will not run at all before your test
106    * ends.
107    *
108    * @param looper the looper to pause
109    */
pauseLooper(Looper looper)110   public static void pauseLooper(Looper looper) {
111     shadowLooper(looper).pause();
112   }
113 
114   /**
115    * Puts the shadow looper in an "unpaused" state (this is the default state). This means that
116    * during tests, tasks sent to the looper will execute inline, immediately, on the calling (main)
117    * thread instead of being queued, in a way similar to how Guava's "DirectExecutorService" works.
118    * This is likely not to be what you want: it will cause code to be potentially executed in a
119    * different order than how it would execute on the device, and if you are using certain Android
120    * APIs (such as view animations) that are non-reentrant, they may not work at all or do
121    * unpredictable things. For more information, see <a
122    * href="https://github.com/robolectric/robolectric/issues/3369">this discussion</a>.
123    *
124    * @param looper the looper to pause
125    */
unPauseLooper(Looper looper)126   public static void unPauseLooper(Looper looper) {
127     shadowLooper(looper).unPause();
128   }
129 
130   /**
131    * Puts the main ShadowLegacyLooper in an "paused" state.
132    *
133    * @see #pauseLooper
134    */
pauseMainLooper()135   public static void pauseMainLooper() {
136     getShadowMainLooper().pause();
137   }
138 
139   /**
140    * Puts the main ShadowLegacyLooper in an "unpaused" state.
141    *
142    * @see #unPauseLooper
143    */
unPauseMainLooper()144   public static void unPauseMainLooper() {
145     getShadowMainLooper().unPause();
146   }
147 
idleMainLooper()148   public static void idleMainLooper() {
149     getShadowMainLooper().idle();
150   }
151 
152   /**
153    * @deprecated Use {@link #idleMainLooper(long, TimeUnit)}.
154    */
155   @InlineMe(
156       replacement = "ShadowLooper.idleMainLooper(interval, MILLISECONDS)",
157       imports = "org.robolectric.shadows.ShadowLooper",
158       staticImports = "java.util.concurrent.TimeUnit.MILLISECONDS")
159   @Deprecated
idleMainLooper(long interval)160   public static void idleMainLooper(long interval) {
161     idleMainLooper(interval, MILLISECONDS);
162   }
163 
idleMainLooper(long amount, TimeUnit unit)164   public static void idleMainLooper(long amount, TimeUnit unit) {
165     getShadowMainLooper().idleFor(amount, unit);
166   }
167 
idleMainLooperConstantly(boolean shouldIdleConstantly)168   public static void idleMainLooperConstantly(boolean shouldIdleConstantly) {
169     getShadowMainLooper().idleConstantly(shouldIdleConstantly);
170   }
171 
runMainLooperOneTask()172   public static void runMainLooperOneTask() {
173     getShadowMainLooper().runOneTask();
174   }
175 
runMainLooperToNextTask()176   public static void runMainLooperToNextTask() {
177     getShadowMainLooper().runToNextTask();
178   }
179 
180   /**
181    * Runs any immediately runnable tasks previously queued on the UI thread, e.g. by {@link
182    * android.app.Activity#runOnUiThread(Runnable)} or {@link
183    * android.os.AsyncTask#onPostExecute(Object)}.
184    *
185    * <p>**Note:** calling this method does not pause or un-pause the scheduler.
186    *
187    * @see #runUiThreadTasksIncludingDelayedTasks
188    */
runUiThreadTasks()189   public static void runUiThreadTasks() {
190     getShadowMainLooper().idle();
191   }
192 
193   /**
194    * Runs all runnable tasks (pending and future) that have been queued on the UI thread. Such tasks
195    * may be queued by e.g. {@link android.app.Activity#runOnUiThread(Runnable)} or {@link
196    * android.os.AsyncTask#onPostExecute(Object)}.
197    *
198    * <p>**Note:** calling this method does not pause or un-pause the scheduler, however the clock is
199    * advanced as future tasks are run.
200    *
201    * @see #runUiThreadTasks
202    */
runUiThreadTasksIncludingDelayedTasks()203   public static void runUiThreadTasksIncludingDelayedTasks() {
204     getShadowMainLooper().runToEndOfTasks();
205   }
206 
quitUnchecked()207   public abstract void quitUnchecked();
208 
hasQuit()209   public abstract boolean hasQuit();
210 
211   /** Executes all posted tasks scheduled before or at the current time. */
idle()212   public abstract void idle();
213 
214   /**
215    * Advances the system clock by the given time, then executes all posted tasks scheduled before or
216    * at the given time.
217    */
idleFor(long time, TimeUnit timeUnit)218   public abstract void idleFor(long time, TimeUnit timeUnit);
219 
220   /** A variant of {@link #idleFor(long, TimeUnit)} that accepts a Duration. */
221   @SuppressWarnings("AndroidJdkLibsChecker")
idleFor(Duration duration)222   public void idleFor(Duration duration) {
223     idleFor(duration.toMillis(), TimeUnit.MILLISECONDS);
224   }
225 
226   /** Returns true if there are no pending tasks scheduled to be executed before current time. */
isIdle()227   public abstract boolean isIdle();
228 
229   /** Not supported for the main Looper in {@link LooperMode.Mode.PAUSED}. */
unPause()230   public abstract void unPause();
231 
isPaused()232   public abstract boolean isPaused();
233 
234   /**
235    * Control the paused state of the Looper.
236    *
237    * <p>Not supported for the main Looper in {@link LooperMode.Mode.PAUSED}.
238    */
setPaused(boolean shouldPause)239   public abstract boolean setPaused(boolean shouldPause);
240 
241   /** Only supported for {@link LooperMode.Mode.LEGACY}. */
resetScheduler()242   public abstract void resetScheduler();
243 
244   /** Causes all enqueued tasks to be discarded, and pause state to be reset */
reset()245   public abstract void reset();
246 
247   /**
248    * Returns the {@link org.robolectric.util.Scheduler} that is being used to manage the enqueued
249    * tasks. This scheduler is managed by the Looper's associated queue.
250    *
251    * <p>Only supported for {@link LooperMode.Mode.LEGACY}.
252    *
253    * @return the {@link org.robolectric.util.Scheduler} that is being used to manage the enqueued
254    *     tasks.
255    */
getScheduler()256   public abstract Scheduler getScheduler();
257 
258   /**
259    * Runs the current task with the looper paused.
260    *
261    * <p>When LooperMode is PAUSED, this will execute all pending tasks scheduled before the current
262    * time.
263    */
runPaused(Runnable run)264   public abstract void runPaused(Runnable run);
265 
266   /**
267    * Helper method to selectively call idle() only if LooperMode is PAUSED.
268    *
269    * <p>Intended for backwards compatibility, to avoid changing behavior for tests still using
270    * LEGACY LooperMode.
271    */
idleIfPaused()272   public abstract void idleIfPaused();
273 
274   /**
275    * Causes {@link Runnable}s that have been scheduled to run within the next {@code intervalMillis}
276    * milliseconds to run while advancing the scheduler's clock.
277    *
278    * @deprecated Use {@link #idleFor(Duration)}.
279    */
280   @Deprecated
281   @InlineMe(
282       replacement = "this.idleFor(Duration.ofMillis(intervalMillis))",
283       imports = "java.time.Duration")
idle(long intervalMillis)284   public final void idle(long intervalMillis) {
285     idleFor(Duration.ofMillis(intervalMillis));
286   }
287 
288   /**
289    * Causes {@link Runnable}s that have been scheduled to run within the next specified amount of
290    * time to run while advancing the clock.
291    *
292    * @deprecated use {@link #idleFor(long, TimeUnit)}
293    */
294   @Deprecated
295   @InlineMe(replacement = "this.idleFor(amount, unit)")
idle(long amount, TimeUnit unit)296   public final void idle(long amount, TimeUnit unit) {
297     idleFor(amount, unit);
298   }
299 
idleConstantly(boolean shouldIdleConstantly)300   public abstract void idleConstantly(boolean shouldIdleConstantly);
301 
302   /**
303    * Causes all of the {@link Runnable}s that have been scheduled to run while advancing the clock
304    * to the start time of the last scheduled {@link Runnable}.
305    */
runToEndOfTasks()306   public abstract void runToEndOfTasks();
307 
308   /**
309    * Causes the next {@link Runnable}(s) that have been scheduled to run while advancing the clock
310    * to its start time. If more than one {@link Runnable} is scheduled to run at this time then they
311    * will all be run.
312    */
runToNextTask()313   public abstract void runToNextTask();
314 
315   /**
316    * Causes only one of the next {@link Runnable}s that have been scheduled to run while advancing
317    * the clock to its start time. Only one {@link Runnable} will run even if more than one has been
318    * scheduled to run at the same time.
319    */
runOneTask()320   public abstract void runOneTask();
321 
322   /**
323    * Enqueue a task to be run later.
324    *
325    * @param runnable the task to be run
326    * @param delayMillis how many milliseconds into the (virtual) future to run it
327    * @return true if the runnable is enqueued
328    * @see android.os.Handler#postDelayed(Runnable,long)
329    * @deprecated Use a {@link android.os.Handler} instance to post to a looper.
330    */
331   @Deprecated
post(Runnable runnable, long delayMillis)332   public abstract boolean post(Runnable runnable, long delayMillis);
333 
334   /**
335    * Enqueue a task to be run ahead of all other delayed tasks.
336    *
337    * @param runnable the task to be run
338    * @return true if the runnable is enqueued
339    * @see android.os.Handler#postAtFrontOfQueue(Runnable)
340    * @deprecated Use a {@link android.os.Handler} instance to post to a looper.
341    */
342   @Deprecated
postAtFrontOfQueue(Runnable runnable)343   public abstract boolean postAtFrontOfQueue(Runnable runnable);
344 
345   /**
346    * Pause the looper.
347    *
348    * <p>Has no practical effect for realistic looper, since it is always paused.
349    */
pause()350   public abstract void pause();
351 
352   /**
353    * @return the scheduled time of the next posted task; Duration.ZERO if there is no currently
354    *     scheduled task.
355    */
getNextScheduledTaskTime()356   public abstract Duration getNextScheduledTaskTime();
357 
358   /**
359    * @return the scheduled time of the last posted task; Duration.ZERO 0 if there is no currently
360    *     scheduled task.
361    */
getLastScheduledTaskTime()362   public abstract Duration getLastScheduledTaskTime();
363 
364   public static class Picker extends LooperShadowPicker<ShadowLooper> {
365 
Picker()366     public Picker() {
367       super(ShadowLegacyLooper.class, ShadowPausedLooper.class);
368     }
369   }
370 }
371