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