1 package org.robolectric.shadows; 2 3 import static android.os.Looper.getMainLooper; 4 import static java.util.concurrent.TimeUnit.MILLISECONDS; 5 import static org.robolectric.annotation.LooperMode.Mode.LEGACY; 6 7 import android.os.Looper; 8 import com.google.errorprone.annotations.InlineMe; 9 import com.google.errorprone.annotations.concurrent.GuardedBy; 10 import java.time.Duration; 11 import java.util.Collection; 12 import java.util.concurrent.TimeUnit; 13 import org.robolectric.annotation.Implements; 14 import org.robolectric.annotation.LooperMode; 15 import org.robolectric.annotation.Resetter; 16 import org.robolectric.config.ConfigurationRegistry; 17 import org.robolectric.shadow.api.Shadow; 18 import org.robolectric.util.Scheduler; 19 20 /** 21 * The base shadow API class for controlling Loopers. 22 * 23 * <p>It will delegate calls to the appropriate shadow based on the current LooperMode. 24 */ 25 @Implements(value = Looper.class, shadowPicker = ShadowLooper.Picker.class) 26 public abstract class ShadowLooper { 27 28 // cache for looperMode(), since this can be an expensive call. 29 @GuardedBy("looperModeLock") 30 private static LooperMode.Mode cachedLooperMode = null; 31 32 private static final Object looperModeLock = new Object(); 33 assertLooperMode(LooperMode.Mode expectedMode)34 public static void assertLooperMode(LooperMode.Mode expectedMode) { 35 if (looperMode() != expectedMode) { 36 throw new IllegalStateException("this action is not supported in " + looperMode() + " mode."); 37 } 38 } 39 shadowLooper(Looper looper)40 private static ShadowLooper shadowLooper(Looper looper) { 41 return Shadow.extract(looper); 42 } 43 44 /** 45 * @deprecated Use {@code shadowOf({@link Looper#getMainLooper()})} instead. 46 */ 47 @Deprecated getShadowMainLooper()48 public static ShadowLooper getShadowMainLooper() { 49 return shadowLooper(getMainLooper()); 50 } 51 52 // TODO: should probably remove this shadowMainLooper()53 public static ShadowLooper shadowMainLooper() { 54 return shadowLooper(getMainLooper()); 55 } 56 getLooperForThread(Thread thread)57 public static Looper getLooperForThread(Thread thread) { 58 if (looperMode() == LEGACY) { 59 return ShadowLegacyLooper.getLooperForThread(thread); 60 } 61 throw new UnsupportedOperationException( 62 "this action is not supported in " + looperMode() + " mode."); 63 } 64 65 /** Return all created loopers. */ getAllLoopers()66 public static Collection<Looper> getAllLoopers() { 67 if (looperMode() == LEGACY) { 68 return ShadowLegacyLooper.getLoopers(); 69 } else { 70 return ShadowPausedLooper.getLoopers(); 71 } 72 } 73 74 /** Should not be called directly - Robolectric internal use only. */ resetThreadLoopers()75 public static void resetThreadLoopers() { 76 if (looperMode() == LEGACY) { 77 ShadowLegacyLooper.resetThreadLoopers(); 78 return; 79 } 80 throw new UnsupportedOperationException( 81 "this action is not supported in " + looperMode() + " mode."); 82 } 83 84 /** Return the current {@link LooperMode}. */ looperMode()85 public static LooperMode.Mode looperMode() { 86 synchronized (looperModeLock) { 87 if (cachedLooperMode == null) { 88 cachedLooperMode = ConfigurationRegistry.get(LooperMode.Mode.class); 89 } 90 return cachedLooperMode; 91 } 92 } 93 94 @Resetter clearLooperMode()95 public static synchronized void clearLooperMode() { 96 synchronized (looperModeLock) { 97 cachedLooperMode = null; 98 } 99 } 100 101 /** 102 * Pauses execution of tasks posted to the ShadowLegacyLooper. This means that during tests, tasks 103 * sent to the looper will not execute immediately, but will be queued in a way that is similar to 104 * how a real looper works. These queued tasks must be executed explicitly by calling {@link 105 * #runToEndOftasks} or a similar method, otherwise they will not run at all before your test 106 * ends. 107 * 108 * @param looper the looper to pause 109 */ pauseLooper(Looper looper)110 public static void pauseLooper(Looper looper) { 111 shadowLooper(looper).pause(); 112 } 113 114 /** 115 * Puts the shadow looper in an "unpaused" state (this is the default state). This means that 116 * during tests, tasks sent to the looper will execute inline, immediately, on the calling (main) 117 * thread instead of being queued, in a way similar to how Guava's "DirectExecutorService" works. 118 * This is likely not to be what you want: it will cause code to be potentially executed in a 119 * different order than how it would execute on the device, and if you are using certain Android 120 * APIs (such as view animations) that are non-reentrant, they may not work at all or do 121 * unpredictable things. For more information, see <a 122 * href="https://github.com/robolectric/robolectric/issues/3369">this discussion</a>. 123 * 124 * @param looper the looper to pause 125 */ unPauseLooper(Looper looper)126 public static void unPauseLooper(Looper looper) { 127 shadowLooper(looper).unPause(); 128 } 129 130 /** 131 * Puts the main ShadowLegacyLooper in an "paused" state. 132 * 133 * @see #pauseLooper 134 */ pauseMainLooper()135 public static void pauseMainLooper() { 136 getShadowMainLooper().pause(); 137 } 138 139 /** 140 * Puts the main ShadowLegacyLooper in an "unpaused" state. 141 * 142 * @see #unPauseLooper 143 */ unPauseMainLooper()144 public static void unPauseMainLooper() { 145 getShadowMainLooper().unPause(); 146 } 147 idleMainLooper()148 public static void idleMainLooper() { 149 getShadowMainLooper().idle(); 150 } 151 152 /** 153 * @deprecated Use {@link #idleMainLooper(long, TimeUnit)}. 154 */ 155 @InlineMe( 156 replacement = "ShadowLooper.idleMainLooper(interval, MILLISECONDS)", 157 imports = "org.robolectric.shadows.ShadowLooper", 158 staticImports = "java.util.concurrent.TimeUnit.MILLISECONDS") 159 @Deprecated idleMainLooper(long interval)160 public static void idleMainLooper(long interval) { 161 idleMainLooper(interval, MILLISECONDS); 162 } 163 idleMainLooper(long amount, TimeUnit unit)164 public static void idleMainLooper(long amount, TimeUnit unit) { 165 getShadowMainLooper().idleFor(amount, unit); 166 } 167 idleMainLooperConstantly(boolean shouldIdleConstantly)168 public static void idleMainLooperConstantly(boolean shouldIdleConstantly) { 169 getShadowMainLooper().idleConstantly(shouldIdleConstantly); 170 } 171 runMainLooperOneTask()172 public static void runMainLooperOneTask() { 173 getShadowMainLooper().runOneTask(); 174 } 175 runMainLooperToNextTask()176 public static void runMainLooperToNextTask() { 177 getShadowMainLooper().runToNextTask(); 178 } 179 180 /** 181 * Runs any immediately runnable tasks previously queued on the UI thread, e.g. by {@link 182 * android.app.Activity#runOnUiThread(Runnable)} or {@link 183 * android.os.AsyncTask#onPostExecute(Object)}. 184 * 185 * <p>**Note:** calling this method does not pause or un-pause the scheduler. 186 * 187 * @see #runUiThreadTasksIncludingDelayedTasks 188 */ runUiThreadTasks()189 public static void runUiThreadTasks() { 190 getShadowMainLooper().idle(); 191 } 192 193 /** 194 * Runs all runnable tasks (pending and future) that have been queued on the UI thread. Such tasks 195 * may be queued by e.g. {@link android.app.Activity#runOnUiThread(Runnable)} or {@link 196 * android.os.AsyncTask#onPostExecute(Object)}. 197 * 198 * <p>**Note:** calling this method does not pause or un-pause the scheduler, however the clock is 199 * advanced as future tasks are run. 200 * 201 * @see #runUiThreadTasks 202 */ runUiThreadTasksIncludingDelayedTasks()203 public static void runUiThreadTasksIncludingDelayedTasks() { 204 getShadowMainLooper().runToEndOfTasks(); 205 } 206 quitUnchecked()207 public abstract void quitUnchecked(); 208 hasQuit()209 public abstract boolean hasQuit(); 210 211 /** Executes all posted tasks scheduled before or at the current time. */ idle()212 public abstract void idle(); 213 214 /** 215 * Advances the system clock by the given time, then executes all posted tasks scheduled before or 216 * at the given time. 217 */ idleFor(long time, TimeUnit timeUnit)218 public abstract void idleFor(long time, TimeUnit timeUnit); 219 220 /** A variant of {@link #idleFor(long, TimeUnit)} that accepts a Duration. */ 221 @SuppressWarnings("AndroidJdkLibsChecker") idleFor(Duration duration)222 public void idleFor(Duration duration) { 223 idleFor(duration.toMillis(), TimeUnit.MILLISECONDS); 224 } 225 226 /** Returns true if there are no pending tasks scheduled to be executed before current time. */ isIdle()227 public abstract boolean isIdle(); 228 229 /** Not supported for the main Looper in {@link LooperMode.Mode.PAUSED}. */ unPause()230 public abstract void unPause(); 231 isPaused()232 public abstract boolean isPaused(); 233 234 /** 235 * Control the paused state of the Looper. 236 * 237 * <p>Not supported for the main Looper in {@link LooperMode.Mode.PAUSED}. 238 */ setPaused(boolean shouldPause)239 public abstract boolean setPaused(boolean shouldPause); 240 241 /** Only supported for {@link LooperMode.Mode.LEGACY}. */ resetScheduler()242 public abstract void resetScheduler(); 243 244 /** Causes all enqueued tasks to be discarded, and pause state to be reset */ reset()245 public abstract void reset(); 246 247 /** 248 * Returns the {@link org.robolectric.util.Scheduler} that is being used to manage the enqueued 249 * tasks. This scheduler is managed by the Looper's associated queue. 250 * 251 * <p>Only supported for {@link LooperMode.Mode.LEGACY}. 252 * 253 * @return the {@link org.robolectric.util.Scheduler} that is being used to manage the enqueued 254 * tasks. 255 */ getScheduler()256 public abstract Scheduler getScheduler(); 257 258 /** 259 * Runs the current task with the looper paused. 260 * 261 * <p>When LooperMode is PAUSED, this will execute all pending tasks scheduled before the current 262 * time. 263 */ runPaused(Runnable run)264 public abstract void runPaused(Runnable run); 265 266 /** 267 * Helper method to selectively call idle() only if LooperMode is PAUSED. 268 * 269 * <p>Intended for backwards compatibility, to avoid changing behavior for tests still using 270 * LEGACY LooperMode. 271 */ idleIfPaused()272 public abstract void idleIfPaused(); 273 274 /** 275 * Causes {@link Runnable}s that have been scheduled to run within the next {@code intervalMillis} 276 * milliseconds to run while advancing the scheduler's clock. 277 * 278 * @deprecated Use {@link #idleFor(Duration)}. 279 */ 280 @Deprecated 281 @InlineMe( 282 replacement = "this.idleFor(Duration.ofMillis(intervalMillis))", 283 imports = "java.time.Duration") idle(long intervalMillis)284 public final void idle(long intervalMillis) { 285 idleFor(Duration.ofMillis(intervalMillis)); 286 } 287 288 /** 289 * Causes {@link Runnable}s that have been scheduled to run within the next specified amount of 290 * time to run while advancing the clock. 291 * 292 * @deprecated use {@link #idleFor(long, TimeUnit)} 293 */ 294 @Deprecated 295 @InlineMe(replacement = "this.idleFor(amount, unit)") idle(long amount, TimeUnit unit)296 public final void idle(long amount, TimeUnit unit) { 297 idleFor(amount, unit); 298 } 299 idleConstantly(boolean shouldIdleConstantly)300 public abstract void idleConstantly(boolean shouldIdleConstantly); 301 302 /** 303 * Causes all of the {@link Runnable}s that have been scheduled to run while advancing the clock 304 * to the start time of the last scheduled {@link Runnable}. 305 */ runToEndOfTasks()306 public abstract void runToEndOfTasks(); 307 308 /** 309 * Causes the next {@link Runnable}(s) that have been scheduled to run while advancing the clock 310 * to its start time. If more than one {@link Runnable} is scheduled to run at this time then they 311 * will all be run. 312 */ runToNextTask()313 public abstract void runToNextTask(); 314 315 /** 316 * Causes only one of the next {@link Runnable}s that have been scheduled to run while advancing 317 * the clock to its start time. Only one {@link Runnable} will run even if more than one has been 318 * scheduled to run at the same time. 319 */ runOneTask()320 public abstract void runOneTask(); 321 322 /** 323 * Enqueue a task to be run later. 324 * 325 * @param runnable the task to be run 326 * @param delayMillis how many milliseconds into the (virtual) future to run it 327 * @return true if the runnable is enqueued 328 * @see android.os.Handler#postDelayed(Runnable,long) 329 * @deprecated Use a {@link android.os.Handler} instance to post to a looper. 330 */ 331 @Deprecated post(Runnable runnable, long delayMillis)332 public abstract boolean post(Runnable runnable, long delayMillis); 333 334 /** 335 * Enqueue a task to be run ahead of all other delayed tasks. 336 * 337 * @param runnable the task to be run 338 * @return true if the runnable is enqueued 339 * @see android.os.Handler#postAtFrontOfQueue(Runnable) 340 * @deprecated Use a {@link android.os.Handler} instance to post to a looper. 341 */ 342 @Deprecated postAtFrontOfQueue(Runnable runnable)343 public abstract boolean postAtFrontOfQueue(Runnable runnable); 344 345 /** 346 * Pause the looper. 347 * 348 * <p>Has no practical effect for realistic looper, since it is always paused. 349 */ pause()350 public abstract void pause(); 351 352 /** 353 * @return the scheduled time of the next posted task; Duration.ZERO if there is no currently 354 * scheduled task. 355 */ getNextScheduledTaskTime()356 public abstract Duration getNextScheduledTaskTime(); 357 358 /** 359 * @return the scheduled time of the last posted task; Duration.ZERO 0 if there is no currently 360 * scheduled task. 361 */ getLastScheduledTaskTime()362 public abstract Duration getLastScheduledTaskTime(); 363 364 public static class Picker extends LooperShadowPicker<ShadowLooper> { 365 Picker()366 public Picker() { 367 super(ShadowLegacyLooper.class, ShadowPausedLooper.class); 368 } 369 } 370 } 371