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