• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.robolectric.util;
2 
3 import static org.robolectric.util.Scheduler.IdleState.CONSTANT_IDLE;
4 import static org.robolectric.util.Scheduler.IdleState.PAUSED;
5 import static org.robolectric.util.Scheduler.IdleState.UNPAUSED;
6 
7 import java.util.Iterator;
8 import java.util.PriorityQueue;
9 import java.util.concurrent.TimeUnit;
10 
11 /**
12  * Class that manages a queue of Runnables that are scheduled to run now (or at some time in the
13  * future). Runnables that are scheduled to run on the UI thread (tasks, animations, etc) eventually
14  * get routed to a Scheduler instance.
15  *
16  * <p>The execution of a scheduler can be in one of three states:
17  *
18  * <ul>
19  *   <li>paused ({@link #pause()}): if paused, then no posted events will be run unless the
20  *       Scheduler is explicitly instructed to do so, correctly matching Android's behavior.
21  *   <li>normal ({@link #unPause()}): if not paused but not set to idle constantly, then the
22  *       Scheduler will automatically run any {@link Runnable}s that are scheduled to run at or
23  *       before the Scheduler's current time, but it won't automatically run any future events. To
24  *       run future events the Scheduler needs to have its clock advanced.
25  *   <li>idling constantly: if {@link #idleConstantly(boolean)} is called with <tt>true</tt>, then
26  *       the Scheduler will continue looping through posted events (including future events),
27  *       advancing its clock as it goes.
28  * </ul>
29  */
30 public class Scheduler {
31 
32   /**
33    * Describes the current state of a {@link Scheduler}.
34    */
35   public enum IdleState {
36     /**
37      * The <tt>Scheduler</tt> will not automatically advance the clock nor execute any runnables.
38      */
39     PAUSED,
40     /**
41      * The <tt>Scheduler</tt>'s clock won't automatically advance the clock but will automatically
42      * execute any runnables scheduled to execute at or before the current time.
43      */
44     UNPAUSED,
45     /**
46      * The <tt>Scheduler</tt> will automatically execute any runnables (past, present or future)
47      * as soon as they are posted and advance the clock if necessary.
48      */
49     CONSTANT_IDLE
50   }
51 
52   private static final long START_TIME = 100;
53   private volatile long currentTime = START_TIME;
54   /**
55    * PriorityQueue doesn't maintain ordering based on insertion; track that ourselves to preserve
56    * FIFO order for posted runnables with the same scheduled time.
57    */
58   private long nextTimeDisambiguator = 0;
59 
60   private boolean isExecutingRunnable = false;
61   private final Thread associatedThread = Thread.currentThread();
62   private final PriorityQueue<ScheduledRunnable> runnables = new PriorityQueue<>();
63   private volatile IdleState idleState = UNPAUSED;
64 
65   /**
66    * Retrieves the current idling state of this <tt>Scheduler</tt>.
67    * @return The current idle state of this <tt>Scheduler</tt>.
68    * @see #setIdleState(IdleState)
69    * @see #isPaused()
70    */
getIdleState()71   public IdleState getIdleState() {
72     return idleState;
73   }
74 
75   /**
76    * Sets the current idling state of this <tt>Scheduler</tt>. If transitioning to the
77    * {@link IdleState#UNPAUSED} state any tasks scheduled to be run at or before the current time
78    * will be run, and if transitioning to the {@link IdleState#CONSTANT_IDLE} state all scheduled
79    * tasks will be run and the clock advanced to the time of the last runnable.
80    * @param idleState The new idle state of this <tt>Scheduler</tt>.
81    * @see #setIdleState(IdleState)
82    * @see #isPaused()
83    */
setIdleState(IdleState idleState)84   public synchronized void setIdleState(IdleState idleState) {
85     this.idleState = idleState;
86     switch (idleState) {
87       case UNPAUSED:
88         advanceBy(0);
89         break;
90       case CONSTANT_IDLE:
91         advanceToLastPostedRunnable();
92         break;
93       default:
94     }
95   }
96 
97   /**
98    * Get the current time (as seen by the scheduler), in milliseconds.
99    *
100    * @return  Current time in milliseconds.
101    */
getCurrentTime()102   public long getCurrentTime() {
103     return currentTime;
104   }
105 
106   /**
107    * Pause the scheduler. Equivalent to <tt>setIdleState(PAUSED)</tt>.
108    *
109    * @see #unPause()
110    * @see #setIdleState(IdleState)
111    */
pause()112   public synchronized void pause() {
113     setIdleState(PAUSED);
114   }
115 
116   /**
117    * Un-pause the scheduler. Equivalent to <tt>setIdleState(UNPAUSED)</tt>.
118    *
119    * @see #pause()
120    * @see #setIdleState(IdleState)
121    */
unPause()122   public synchronized void unPause() {
123     setIdleState(UNPAUSED);
124   }
125 
126   /**
127    * Determine if the scheduler is paused.
128    *
129    * @return  <tt>true</tt> if it is paused.
130    */
isPaused()131   public boolean isPaused() {
132     return idleState == PAUSED;
133   }
134 
135   /**
136    * Add a runnable to the queue.
137    *
138    * @param runnable    Runnable to add.
139    */
post(Runnable runnable)140   public synchronized void post(Runnable runnable) {
141     postDelayed(runnable, 0, TimeUnit.MILLISECONDS);
142   }
143 
144   /**
145    * Add a runnable to the queue to be run after a delay.
146    *
147    * @param runnable    Runnable to add.
148    * @param delayMillis Delay in millis.
149    */
postDelayed(Runnable runnable, long delayMillis)150   public synchronized void postDelayed(Runnable runnable, long delayMillis) {
151     postDelayed(runnable, delayMillis, TimeUnit.MILLISECONDS);
152   }
153 
154   /**
155    * Add a runnable to the queue to be run after a delay.
156    */
postDelayed(Runnable runnable, long delay, TimeUnit unit)157   public synchronized void postDelayed(Runnable runnable, long delay, TimeUnit unit) {
158     long delayMillis = unit.toMillis(delay);
159     if ((idleState != CONSTANT_IDLE && (isPaused() || delayMillis > 0)) || Thread.currentThread() != associatedThread) {
160       runnables.add(new ScheduledRunnable(runnable, currentTime + delayMillis));
161     } else {
162       runOrQueueRunnable(runnable, currentTime + delayMillis);
163     }
164   }
165 
166   /**
167    * Add a runnable to the head of the queue.
168    *
169    * @param runnable  Runnable to add.
170    */
postAtFrontOfQueue(Runnable runnable)171   public synchronized void postAtFrontOfQueue(Runnable runnable) {
172     if (isPaused() || Thread.currentThread() != associatedThread) {
173       final long timeDisambiguator;
174       if (runnables.isEmpty()) {
175         timeDisambiguator = nextTimeDisambiguator++;
176       } else {
177         timeDisambiguator = runnables.peek().timeDisambiguator - 1;
178       }
179       runnables.add(new ScheduledRunnable(runnable, 0, timeDisambiguator));
180     } else {
181       runOrQueueRunnable(runnable, currentTime);
182     }
183   }
184 
185   /**
186    * Remove a runnable from the queue.
187    *
188    * @param runnable  Runnable to remove.
189    */
remove(Runnable runnable)190   public synchronized void remove(Runnable runnable) {
191     Iterator<ScheduledRunnable> iterator = runnables.iterator();
192     while (iterator.hasNext()) {
193       if (iterator.next().runnable == runnable) {
194         iterator.remove();
195       }
196     }
197   }
198 
199   /**
200    * Run all runnables in the queue, and any additional runnables they schedule that are scheduled
201    * before the latest scheduled runnable currently in the queue.
202    *
203    * @return True if a runnable was executed.
204    */
advanceToLastPostedRunnable()205   public synchronized boolean advanceToLastPostedRunnable() {
206     long currentMaxTime = currentTime;
207     for (ScheduledRunnable scheduled : runnables) {
208       if (currentMaxTime < scheduled.scheduledTime) {
209         currentMaxTime = scheduled.scheduledTime;
210       }
211     }
212     return advanceTo(currentMaxTime);
213   }
214 
215   /**
216    * Run the next runnable in the queue.
217    *
218    * @return  True if a runnable was executed.
219    */
advanceToNextPostedRunnable()220   public synchronized boolean advanceToNextPostedRunnable() {
221     return !runnables.isEmpty() && advanceTo(runnables.peek().scheduledTime);
222   }
223 
224   /**
225    * Run all runnables that are scheduled to run in the next time interval.
226    *
227    * @param   interval  Time interval (in millis).
228    * @return  True if a runnable was executed.
229    * @deprecated Use {@link #advanceBy(long, TimeUnit)}.
230    */
231   @Deprecated
advanceBy(long interval)232   public synchronized boolean advanceBy(long interval) {
233     return advanceBy(interval, TimeUnit.MILLISECONDS);
234   }
235 
236   /**
237    * Run all runnables that are scheduled to run in the next time interval.
238    *
239    * @return  True if a runnable was executed.
240    */
advanceBy(long amount, TimeUnit unit)241   public synchronized boolean advanceBy(long amount, TimeUnit unit) {
242     long endingTime = currentTime + unit.toMillis(amount);
243     return advanceTo(endingTime);
244   }
245 
246   /**
247    * Run all runnables that are scheduled before the endTime.
248    *
249    * @param   endTime   Future time.
250    * @return  True if a runnable was executed.
251    */
advanceTo(long endTime)252   public synchronized boolean advanceTo(long endTime) {
253     if (endTime < currentTime || runnables.isEmpty()) {
254       currentTime = endTime;
255       return false;
256     }
257 
258     int runCount = 0;
259     while (nextTaskIsScheduledBefore(endTime)) {
260       runOneTask();
261       ++runCount;
262     }
263     currentTime = endTime;
264     return runCount > 0;
265   }
266 
267   /**
268    * Run the next runnable in the queue.
269    *
270    * @return  True if a runnable was executed.
271    */
runOneTask()272   public synchronized boolean runOneTask() {
273     ScheduledRunnable postedRunnable = runnables.poll();
274     if (postedRunnable != null) {
275       if (postedRunnable.scheduledTime > currentTime) {
276         currentTime = postedRunnable.scheduledTime;
277       }
278       postedRunnable.run();
279       return true;
280     }
281     return false;
282   }
283 
284   /**
285    * Determine if any enqueued runnables are enqueued before the current time.
286    *
287    * @return  True if any runnables can be executed.
288    */
areAnyRunnable()289   public synchronized boolean areAnyRunnable() {
290     return nextTaskIsScheduledBefore(currentTime);
291   }
292 
293   /**
294    * Reset the internal state of the Scheduler.
295    */
reset()296   public synchronized void reset() {
297     runnables.clear();
298     idleState = UNPAUSED;
299     currentTime = START_TIME;
300     isExecutingRunnable = false;
301   }
302 
303   /**
304    * Return the number of enqueued runnables.
305    *
306    * @return  Number of enqueues runnables.
307    */
size()308   public synchronized int size() {
309     return runnables.size();
310   }
311 
312   /**
313    * Set the idle state of the Scheduler. If necessary, the clock will be advanced and runnables
314    * executed as required by the newly-set state.
315    *
316    * @param shouldIdleConstantly  If <tt>true</tt> the idle state will be set to
317    *                              {@link IdleState#CONSTANT_IDLE}, otherwise it will be set to
318    *                              {@link IdleState#UNPAUSED}.
319    * @deprecated This method is ambiguous in how it should behave when turning off constant idle.
320    * Use {@link #setIdleState(IdleState)} instead to explicitly set the state.
321    */
322   @Deprecated
idleConstantly(boolean shouldIdleConstantly)323   public void idleConstantly(boolean shouldIdleConstantly) {
324     setIdleState(shouldIdleConstantly ? CONSTANT_IDLE : UNPAUSED);
325   }
326 
nextTaskIsScheduledBefore(long endingTime)327   private boolean nextTaskIsScheduledBefore(long endingTime) {
328     return !runnables.isEmpty() && runnables.peek().scheduledTime <= endingTime;
329   }
330 
runOrQueueRunnable(Runnable runnable, long scheduledTime)331   private void runOrQueueRunnable(Runnable runnable, long scheduledTime) {
332     if (isExecutingRunnable) {
333       runnables.add(new ScheduledRunnable(runnable, scheduledTime));
334       return;
335     }
336     isExecutingRunnable = true;
337     try {
338       runnable.run();
339     } finally {
340       isExecutingRunnable = false;
341     }
342     if (scheduledTime > currentTime) {
343       currentTime = scheduledTime;
344     }
345     // The runnable we just ran may have queued other runnables. If there are
346     // any pending immediate execution we should run these now too, unless we are
347     // paused.
348     switch (idleState) {
349       case CONSTANT_IDLE:
350         advanceToLastPostedRunnable();
351         break;
352       case UNPAUSED:
353         advanceBy(0);
354         break;
355       default:
356     }
357   }
358 
359   private class ScheduledRunnable implements Comparable<ScheduledRunnable> {
360     private final Runnable runnable;
361     private final long scheduledTime;
362     private final long timeDisambiguator;
363 
ScheduledRunnable(Runnable runnable, long scheduledTime)364     private ScheduledRunnable(Runnable runnable, long scheduledTime) {
365       this(runnable, scheduledTime, nextTimeDisambiguator++);
366     }
367 
ScheduledRunnable(Runnable runnable, long scheduledTime, long timeDisambiguator)368     private ScheduledRunnable(Runnable runnable, long scheduledTime, long timeDisambiguator) {
369       this.runnable = runnable;
370       this.scheduledTime = scheduledTime;
371       this.timeDisambiguator = timeDisambiguator;
372     }
373 
374     @Override
compareTo(ScheduledRunnable runnable)375     public int compareTo(ScheduledRunnable runnable) {
376       int timeCompare = Long.compare(scheduledTime, runnable.scheduledTime);
377       if (timeCompare == 0) {
378         return Long.compare(timeDisambiguator, runnable.timeDisambiguator);
379       }
380       return timeCompare;
381     }
382 
run()383     public void run() {
384       isExecutingRunnable = true;
385       try {
386         runnable.run();
387       } finally {
388         isExecutingRunnable = false;
389       }
390     }
391   }
392 }
393