1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2; 4 import static org.robolectric.RuntimeEnvironment.isMainThread; 5 import static org.robolectric.shadow.api.Shadow.invokeConstructor; 6 import static org.robolectric.util.ReflectionHelpers.ClassParameter.from; 7 8 import android.os.Looper; 9 import android.os.MessageQueue; 10 import java.util.Collections; 11 import java.util.Map; 12 import java.util.WeakHashMap; 13 import java.util.concurrent.TimeUnit; 14 import org.robolectric.RoboSettings; 15 import org.robolectric.RuntimeEnvironment; 16 import org.robolectric.annotation.Implementation; 17 import org.robolectric.annotation.Implements; 18 import org.robolectric.annotation.RealObject; 19 import org.robolectric.annotation.Resetter; 20 import org.robolectric.shadow.api.Shadow; 21 import org.robolectric.util.Scheduler; 22 23 /** 24 * Robolectric enqueues posted {@link Runnable}s to be run 25 * (on this thread) later. {@code Runnable}s that are scheduled to run immediately can be 26 * triggered by calling {@link #idle()}. 27 * 28 * @see ShadowMessageQueue 29 */ 30 @Implements(Looper.class) 31 @SuppressWarnings("SynchronizeOnNonFinalField") 32 public class ShadowLooper { 33 34 // Replaced SoftThreadLocal with a WeakHashMap, because ThreadLocal make it impossible to access their contents from other 35 // threads, but we need to be able to access the loopers for all threads so that we can shut them down when resetThreadLoopers() 36 // is called. This also allows us to implement the useful getLooperForThread() method. 37 // Note that the main looper is handled differently and is not put in this hash, because we need to be able to 38 // "switch" the thread that the main looper is associated with. 39 private static Map<Thread, Looper> loopingLoopers = Collections.synchronizedMap(new WeakHashMap<Thread, Looper>()); 40 41 private static Looper mainLooper; 42 43 private @RealObject Looper realObject; 44 45 boolean quit; 46 47 @Resetter resetThreadLoopers()48 public static synchronized void resetThreadLoopers() { 49 // Blech. We need to keep the main looper because somebody might refer to it in a static 50 // field. The other loopers need to be wrapped in WeakReferences so that they are not prevented from 51 // being garbage collected. 52 if (!isMainThread()) { 53 throw new IllegalStateException("you should only be calling this from the main thread!"); 54 } 55 synchronized (loopingLoopers) { 56 for (Looper looper : loopingLoopers.values()) { 57 synchronized (looper) { 58 if (!shadowOf(looper).quit) { 59 looper.quit(); 60 } else { 61 // Reset the schedulers of all loopers. This prevents un-run tasks queued up in static 62 // background handlers from leaking to subsequent tests. 63 shadowOf(looper).getScheduler().reset(); 64 shadowOf(looper.getQueue()).reset(); 65 } 66 } 67 } 68 } 69 // Because resetStaticState() is called by ParallelUniverse on startup before prepareMainLooper() is 70 // called, this might be null on that occasion. 71 if (mainLooper != null) { 72 shadowOf(mainLooper).reset(); 73 } 74 } 75 76 @Implementation __constructor__(boolean quitAllowed)77 protected void __constructor__(boolean quitAllowed) { 78 invokeConstructor(Looper.class, realObject, from(boolean.class, quitAllowed)); 79 if (isMainThread()) { 80 mainLooper = realObject; 81 } else { 82 loopingLoopers.put(Thread.currentThread(), realObject); 83 } 84 resetScheduler(); 85 } 86 87 @Implementation getMainLooper()88 protected static Looper getMainLooper() { 89 return mainLooper; 90 } 91 92 @Implementation myLooper()93 protected static Looper myLooper() { 94 return getLooperForThread(Thread.currentThread()); 95 } 96 97 @Implementation loop()98 protected static void loop() { 99 shadowOf(Looper.myLooper()).doLoop(); 100 } 101 doLoop()102 private void doLoop() { 103 if (realObject != Looper.getMainLooper()) { 104 synchronized (realObject) { 105 while (!quit) { 106 try { 107 realObject.wait(); 108 } catch (InterruptedException ignore) { 109 } 110 } 111 } 112 } 113 } 114 115 @Implementation quit()116 protected void quit() { 117 if (realObject == Looper.getMainLooper()) throw new RuntimeException("Main thread not allowed to quit"); 118 quitUnchecked(); 119 } 120 121 @Implementation(minSdk = JELLY_BEAN_MR2) quitSafely()122 protected void quitSafely() { 123 quit(); 124 } 125 quitUnchecked()126 public void quitUnchecked() { 127 synchronized (realObject) { 128 quit = true; 129 realObject.notifyAll(); 130 getScheduler().reset(); 131 shadowOf(realObject.getQueue()).reset(); 132 } 133 } 134 hasQuit()135 public boolean hasQuit() { 136 synchronized (realObject) { 137 return quit; 138 } 139 } 140 141 /** @deprecated Use `shadowOf({@link Looper#getMainLooper()})` instead. */ 142 @Deprecated getShadowMainLooper()143 public static ShadowLooper getShadowMainLooper() { 144 return shadowOf(Looper.getMainLooper()); 145 } 146 getLooperForThread(Thread thread)147 public static Looper getLooperForThread(Thread thread) { 148 return isMainThread(thread) ? mainLooper : loopingLoopers.get(thread); 149 } 150 151 /** 152 * Pauses execution of tasks posted to the ShadowLooper. This means that during tests, tasks sent 153 * to the looper will not execute immediately, but will be queued in a way that is similar to how 154 * a real looper works. These queued tasks must be executed explicitly by calling {@link 155 * #runToEndOftasks} or a similar method, otherwise they will not run at all before your test 156 * ends. 157 * 158 * @param looper the looper to pause 159 */ pauseLooper(Looper looper)160 public static void pauseLooper(Looper looper) { 161 shadowOf(looper).pause(); 162 } 163 164 /** 165 * Puts the shadow looper in an "unpaused" state (this is the default state). This means that 166 * during tests, tasks sent to the looper will execute inline, immediately, on the calling (main) 167 * thread instead of being queued, in a way similar to how Guava's "DirectExecutorService" works. 168 * This is likely not to be what you want: it will cause code to be potentially executed in a 169 * different order than how it would execute on the device, and if you are using certain Android 170 * APIs (such as view animations) that are non-reentrant, they may not work at all or do 171 * unpredictable things. For more information, see <a 172 * href="https://github.com/robolectric/robolectric/issues/3369">this discussion</a>. 173 * 174 * @param looper the looper to pause 175 */ unPauseLooper(Looper looper)176 public static void unPauseLooper(Looper looper) { 177 shadowOf(looper).unPause(); 178 } 179 180 /** 181 * Puts the main ShadowLooper in an "paused" state. 182 * 183 * @see #pauseLooper 184 */ pauseMainLooper()185 public static void pauseMainLooper() { 186 getShadowMainLooper().pause(); 187 } 188 189 /** 190 * Puts the main ShadowLooper in an "unpaused" state. 191 * 192 * @see #unPauseLooper 193 */ unPauseMainLooper()194 public static void unPauseMainLooper() { 195 getShadowMainLooper().unPause(); 196 } 197 idleMainLooper()198 public static void idleMainLooper() { 199 getShadowMainLooper().idle(); 200 } 201 202 /** @deprecated Use {@link #idleMainLooper(long, TimeUnit)}. */ 203 @Deprecated idleMainLooper(long interval)204 public static void idleMainLooper(long interval) { 205 idleMainLooper(interval, TimeUnit.MILLISECONDS); 206 } 207 idleMainLooper(long amount, TimeUnit unit)208 public static void idleMainLooper(long amount, TimeUnit unit) { 209 getShadowMainLooper().idle(amount, unit); 210 } 211 idleMainLooperConstantly(boolean shouldIdleConstantly)212 public static void idleMainLooperConstantly(boolean shouldIdleConstantly) { 213 getShadowMainLooper().idleConstantly(shouldIdleConstantly); 214 } 215 runMainLooperOneTask()216 public static void runMainLooperOneTask() { 217 getShadowMainLooper().runOneTask(); 218 } 219 runMainLooperToNextTask()220 public static void runMainLooperToNextTask() { 221 getShadowMainLooper().runToNextTask(); 222 } 223 224 /** 225 * Runs any immediately runnable tasks previously queued on the UI thread, 226 * e.g. by {@link android.app.Activity#runOnUiThread(Runnable)} or {@link android.os.AsyncTask#onPostExecute(Object)}. 227 * 228 * **Note:** calling this method does not pause or un-pause the scheduler. 229 * 230 * @see #runUiThreadTasksIncludingDelayedTasks 231 */ runUiThreadTasks()232 public static void runUiThreadTasks() { 233 getShadowMainLooper().idle(); 234 } 235 236 /** 237 * Runs all runnable tasks (pending and future) that have been queued on the UI thread. Such tasks may be queued by 238 * e.g. {@link android.app.Activity#runOnUiThread(Runnable)} or {@link android.os.AsyncTask#onPostExecute(Object)}. 239 * 240 * **Note:** calling this method does not pause or un-pause the scheduler, however the clock is advanced as 241 * future tasks are run. 242 * 243 * @see #runUiThreadTasks 244 */ runUiThreadTasksIncludingDelayedTasks()245 public static void runUiThreadTasksIncludingDelayedTasks() { 246 getShadowMainLooper().runToEndOfTasks(); 247 } 248 249 /** 250 * Causes {@link Runnable}s that have been scheduled to run immediately to actually run. Does not advance the 251 * scheduler's clock; 252 */ idle()253 public void idle() { 254 idle(0, TimeUnit.MILLISECONDS); 255 } 256 257 /** 258 * Causes {@link Runnable}s that have been scheduled to run within the next {@code intervalMillis} milliseconds to 259 * run while advancing the scheduler's clock. 260 * 261 * @deprecated Use {@link #idle(long, TimeUnit)}. 262 */ 263 @Deprecated idle(long intervalMillis)264 public void idle(long intervalMillis) { 265 idle(intervalMillis, TimeUnit.MILLISECONDS); 266 } 267 268 /** 269 * Causes {@link Runnable}s that have been scheduled to run within the next specified amount of time to run while 270 * advancing the scheduler's clock. 271 */ idle(long amount, TimeUnit unit)272 public void idle(long amount, TimeUnit unit) { 273 getScheduler().advanceBy(amount, unit); 274 } 275 idleConstantly(boolean shouldIdleConstantly)276 public void idleConstantly(boolean shouldIdleConstantly) { 277 getScheduler().idleConstantly(shouldIdleConstantly); 278 } 279 280 /** 281 * Causes all of the {@link Runnable}s that have been scheduled to run while advancing the scheduler's clock to the 282 * start time of the last scheduled {@link Runnable}. 283 */ runToEndOfTasks()284 public void runToEndOfTasks() { 285 getScheduler().advanceToLastPostedRunnable(); 286 } 287 288 /** 289 * Causes the next {@link Runnable}(s) that have been scheduled to run while advancing the scheduler's clock to its 290 * start time. If more than one {@link Runnable} is scheduled to run at this time then they will all be run. 291 */ runToNextTask()292 public void runToNextTask() { 293 getScheduler().advanceToNextPostedRunnable(); 294 } 295 296 /** 297 * Causes only one of the next {@link Runnable}s that have been scheduled to run while advancing the scheduler's 298 * clock to its start time. Only one {@link Runnable} will run even if more than one has ben scheduled to run at the 299 * same time. 300 */ runOneTask()301 public void runOneTask() { 302 getScheduler().runOneTask(); 303 } 304 305 /** 306 * Enqueue a task to be run later. 307 * 308 * @param runnable the task to be run 309 * @param delayMillis how many milliseconds into the (virtual) future to run it 310 * @return true if the runnable is enqueued 311 * @see android.os.Handler#postDelayed(Runnable,long) 312 * @deprecated Use a {@link android.os.Handler} instance to post to a looper. 313 */ 314 @Deprecated post(Runnable runnable, long delayMillis)315 public boolean post(Runnable runnable, long delayMillis) { 316 if (!quit) { 317 getScheduler().postDelayed(runnable, delayMillis, TimeUnit.MILLISECONDS); 318 return true; 319 } else { 320 return false; 321 } 322 } 323 324 /** 325 * Enqueue a task to be run ahead of all other delayed tasks. 326 * 327 * @param runnable the task to be run 328 * @return true if the runnable is enqueued 329 * @see android.os.Handler#postAtFrontOfQueue(Runnable) 330 * @deprecated Use a {@link android.os.Handler} instance to post to a looper. 331 */ 332 @Deprecated postAtFrontOfQueue(Runnable runnable)333 public boolean postAtFrontOfQueue(Runnable runnable) { 334 if (!quit) { 335 getScheduler().postAtFrontOfQueue(runnable); 336 return true; 337 } else { 338 return false; 339 } 340 } 341 pause()342 public void pause() { 343 getScheduler().pause(); 344 } 345 unPause()346 public void unPause() { 347 getScheduler().unPause(); 348 } 349 isPaused()350 public boolean isPaused() { 351 return getScheduler().isPaused(); 352 } 353 setPaused(boolean shouldPause)354 public boolean setPaused(boolean shouldPause) { 355 boolean wasPaused = isPaused(); 356 if (shouldPause) { 357 pause(); 358 } else { 359 unPause(); 360 } 361 return wasPaused; 362 } 363 resetScheduler()364 public void resetScheduler() { 365 ShadowMessageQueue shadowMessageQueue = shadowOf(realObject.getQueue()); 366 if (realObject == Looper.getMainLooper() || RoboSettings.isUseGlobalScheduler()) { 367 shadowMessageQueue.setScheduler(RuntimeEnvironment.getMasterScheduler()); 368 } else { 369 shadowMessageQueue.setScheduler(new Scheduler()); 370 } 371 } 372 373 /** 374 * Causes all enqueued tasks to be discarded, and pause state to be reset 375 */ reset()376 public void reset() { 377 shadowOf(realObject.getQueue()).reset(); 378 resetScheduler(); 379 380 quit = false; 381 } 382 383 /** 384 * Returns the {@link org.robolectric.util.Scheduler} that is being used to manage the enqueued tasks. 385 * This scheduler is managed by the Looper's associated queue. 386 * 387 * @return the {@link org.robolectric.util.Scheduler} that is being used to manage the enqueued tasks. 388 */ getScheduler()389 public Scheduler getScheduler() { 390 return shadowOf(realObject.getQueue()).getScheduler(); 391 } 392 runPaused(Runnable r)393 public void runPaused(Runnable r) { 394 boolean wasPaused = setPaused(true); 395 try { 396 r.run(); 397 } finally { 398 if (!wasPaused) unPause(); 399 } 400 } 401 shadowOf(Looper looper)402 private static ShadowLooper shadowOf(Looper looper) { 403 return Shadow.extract(looper); 404 } 405 shadowOf(MessageQueue mq)406 private static ShadowMessageQueue shadowOf(MessageQueue mq) { 407 return Shadow.extract(mq); 408 } 409 } 410