Home | History | Annotate | Download | only in util
      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    */
     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    */
     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    */
    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    */
    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    */
    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    */
    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    */
    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    */
    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    */
    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    */
    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    */
    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    */
    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    */
    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
    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    */
    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    */
    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    */
    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    */
    289   public synchronized boolean areAnyRunnable() {
    290     return nextTaskIsScheduledBefore(currentTime);
    291   }
    292 
    293   /**
    294    * Reset the internal state of the Scheduler.
    295    */
    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    */
    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
    323   public void idleConstantly(boolean shouldIdleConstantly) {
    324     setIdleState(shouldIdleConstantly ? CONSTANT_IDLE : UNPAUSED);
    325   }
    326 
    327   private boolean nextTaskIsScheduledBefore(long endingTime) {
    328     return !runnables.isEmpty() && runnables.peek().scheduledTime <= endingTime;
    329   }
    330 
    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 
    364     private ScheduledRunnable(Runnable runnable, long scheduledTime) {
    365       this(runnable, scheduledTime, nextTimeDisambiguator++);
    366     }
    367 
    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
    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 
    383     public void run() {
    384       isExecutingRunnable = true;
    385       try {
    386         runnable.run();
    387       } finally {
    388         isExecutingRunnable = false;
    389       }
    390     }
    391   }
    392 }
    393