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