package org.robolectric.util; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.ListIterator; import java.util.concurrent.TimeUnit; import static org.robolectric.util.Scheduler.IdleState.*; /** * Class that manages a queue of Runnables that are scheduled to run now (or at some time in * the future). Runnables that are scheduled to run on the UI thread (tasks, animations, etc) * eventually get routed to a Scheduler instance. * * The execution of a scheduler can be in one of three states: * <ul><li>paused ({@link #pause()}): if paused, then no posted events will be run unless the Scheduler * is explicitly instructed to do so.</li> * <li>normal ({@link #unPause()}): if not paused but not set to idle constantly, then the Scheduler will * automatically run any {@link Runnable}s that are scheduled to run at or before the * Scheduler's current time, but it won't automatically run any future events. To * run future events the Scheduler needs to have its clock advanced.</li> * <li>idling constantly: if {@link #idleConstantly(boolean)} is called with * <tt>true</tt>, then the Scheduler will continue looping through posted events * (including future events), advancing its clock as it goes.</li> * </ul> */ public class Scheduler { /** * Describes the current state of a {@link Scheduler}. */ public enum IdleState { /** * The <tt>Scheduler</tt> will not automatically advance the clock nor execute any runnables. */ PAUSED, /** * The <tt>Scheduler</tt>'s clock won't automatically advance the clock but will automatically * execute any runnables scheduled to execute at or before the current time. */ UNPAUSED, /** * The <tt>Scheduler</tt> will automatically execute any runnables (past, present or future) * as soon as they are posted and advance the clock if necessary. */ CONSTANT_IDLE } private final static long START_TIME = 100; private volatile long currentTime = START_TIME; private boolean isExecutingRunnable = false; private final Thread associatedThread = Thread.currentThread(); private final List<ScheduledRunnable> runnables = new ArrayList<>(); private volatile IdleState idleState = UNPAUSED; /** * Retrieves the current idling state of this <tt>Scheduler</tt>. * @return The current idle state of this <tt>Scheduler</tt>. * @see #setIdleState(IdleState) * @see #isPaused() */ public IdleState getIdleState() { return idleState; } /** * Sets the current idling state of this <tt>Scheduler</tt>. If transitioning to the * {@link IdleState#UNPAUSED} state any tasks scheduled to be run at or before the current time * will be run, and if transitioning to the {@link IdleState#CONSTANT_IDLE} state all scheduled * tasks will be run and the clock advanced to the time of the last runnable. * @param idleState The new idle state of this <tt>Scheduler</tt>. * @see #setIdleState(IdleState) * @see #isPaused() */ public synchronized void setIdleState(IdleState idleState) { this.idleState = idleState; switch (idleState) { case UNPAUSED: advanceBy(0); break; case CONSTANT_IDLE: advanceToLastPostedRunnable(); break; default: } } /** * Get the current time (as seen by the scheduler), in milliseconds. * * @return Current time in milliseconds. */ public long getCurrentTime() { return currentTime; } /** * Pause the scheduler. Equivalent to <tt>setIdleState(PAUSED)</tt>. * * @see #unPause() * @see #setIdleState(IdleState) */ public synchronized void pause() { setIdleState(PAUSED); } /** * Un-pause the scheduler. Equivalent to <tt>setIdleState(UNPAUSED)</tt>. * * @see #pause() * @see #setIdleState(IdleState) */ public synchronized void unPause() { setIdleState(UNPAUSED); } /** * Determine if the scheduler is paused. * * @return <tt>true</tt> if it is paused. */ public boolean isPaused() { return idleState == PAUSED; } /** * Add a runnable to the queue. * * @param runnable Runnable to add. */ public synchronized void post(Runnable runnable) { postDelayed(runnable, 0, TimeUnit.MILLISECONDS); } /** * Add a runnable to the queue to be run after a delay. * * @param runnable Runnable to add. * @param delayMillis Delay in millis. */ public synchronized void postDelayed(Runnable runnable, long delayMillis) { postDelayed(runnable, delayMillis, TimeUnit.MILLISECONDS); } /** * Add a runnable to the queue to be run after a delay. */ public synchronized void postDelayed(Runnable runnable, long delay, TimeUnit unit) { long delayMillis = unit.toMillis(delay); if ((idleState != CONSTANT_IDLE && (isPaused() || delayMillis > 0)) || Thread.currentThread() != associatedThread) { queueRunnableAndSort(runnable, currentTime + delayMillis); } else { runOrQueueRunnable(runnable, currentTime + delayMillis); } } /** * Add a runnable to the head of the queue. * * @param runnable Runnable to add. */ public synchronized void postAtFrontOfQueue(Runnable runnable) { if (isPaused() || Thread.currentThread() != associatedThread) { runnables.add(0, new ScheduledRunnable(runnable, currentTime)); } else { runOrQueueRunnable(runnable, currentTime); } } /** * Remove a runnable from the queue. * * @param runnable Runnable to remove. */ public synchronized void remove(Runnable runnable) { ListIterator<ScheduledRunnable> iterator = runnables.listIterator(); while (iterator.hasNext()) { ScheduledRunnable next = iterator.next(); if (next.runnable == runnable) { iterator.remove(); } } } /** * Run all runnables in the queue. * * @return True if a runnable was executed. */ public synchronized boolean advanceToLastPostedRunnable() { return size() >= 1 && advanceTo(runnables.get(runnables.size() - 1).scheduledTime); } /** * Run the next runnable in the queue. * * @return True if a runnable was executed. */ public synchronized boolean advanceToNextPostedRunnable() { return size() >= 1 && advanceTo(runnables.get(0).scheduledTime); } /** * Run all runnables that are scheduled to run in the next time interval. * * @param interval Time interval (in millis). * @return True if a runnable was executed. * @deprecated Use {@link #advanceBy(long, TimeUnit)}. */ @Deprecated public synchronized boolean advanceBy(long interval) { return advanceBy(interval, TimeUnit.MILLISECONDS); } /** * Run all runnables that are scheduled to run in the next time interval. * * @return True if a runnable was executed. */ public synchronized boolean advanceBy(long amount, TimeUnit unit) { long endingTime = currentTime + unit.toMillis(amount); return advanceTo(endingTime); } /** * Run all runnables that are scheduled before the endTime. * * @param endTime Future time. * @return True if a runnable was executed. */ public synchronized boolean advanceTo(long endTime) { if (endTime - currentTime < 0 || size() < 1) { currentTime = endTime; return false; } int runCount = 0; while (nextTaskIsScheduledBefore(endTime)) { runOneTask(); ++runCount; } currentTime = endTime; return runCount > 0; } /** * Run the next runnable in the queue. * * @return True if a runnable was executed. */ public synchronized boolean runOneTask() { if (size() < 1) { return false; } ScheduledRunnable postedRunnable = runnables.remove(0); currentTime = postedRunnable.scheduledTime; postedRunnable.run(); return true; } /** * Determine if any enqueued runnables are enqueued before the current time. * * @return True if any runnables can be executed. */ public synchronized boolean areAnyRunnable() { return nextTaskIsScheduledBefore(currentTime); } /** * Reset the internal state of the Scheduler. */ public synchronized void reset() { runnables.clear(); idleState = UNPAUSED; currentTime = START_TIME; isExecutingRunnable = false; } /** * Return the number of enqueued runnables. * * @return Number of enqueues runnables. */ public synchronized int size() { return runnables.size(); } /** * Set the idle state of the Scheduler. If necessary, the clock will be advanced and runnables * executed as required by the newly-set state. * * @param shouldIdleConstantly If <tt>true</tt> the idle state will be set to * {@link IdleState#CONSTANT_IDLE}, otherwise it will be set to * {@link IdleState#UNPAUSED}. * @deprecated This method is ambiguous in how it should behave when turning off constant idle. * Use {@link #setIdleState(IdleState)} instead to explicitly set the state. */ @Deprecated public void idleConstantly(boolean shouldIdleConstantly) { setIdleState(shouldIdleConstantly ? CONSTANT_IDLE : UNPAUSED); } private boolean nextTaskIsScheduledBefore(long endingTime) { return size() > 0 && runnables.get(0).scheduledTime <= endingTime; } private void runOrQueueRunnable(Runnable runnable, long scheduledTime) { if (isExecutingRunnable) { queueRunnableAndSort(runnable, scheduledTime); return; } isExecutingRunnable = true; try { runnable.run(); } finally { isExecutingRunnable = false; } if (scheduledTime > currentTime) { currentTime = scheduledTime; } // The runnable we just ran may have queued other runnables. If there are // any pending immediate execution we should run these now too, unless we are // paused. switch (idleState) { case CONSTANT_IDLE: advanceToLastPostedRunnable(); break; case UNPAUSED: advanceBy(0); break; default: } } private void queueRunnableAndSort(Runnable runnable, long scheduledTime) { runnables.add(new ScheduledRunnable(runnable, scheduledTime)); Collections.sort(runnables); } private class ScheduledRunnable implements Comparable<ScheduledRunnable> { private final Runnable runnable; private final long scheduledTime; private ScheduledRunnable(Runnable runnable, long scheduledTime) { this.runnable = runnable; this.scheduledTime = scheduledTime; } @Override public int compareTo(ScheduledRunnable runnable) { return (int) (scheduledTime - runnable.scheduledTime); } public void run() { isExecutingRunnable = true; try { runnable.run(); } finally { isExecutingRunnable = false; } } } }