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