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