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.time.Duration; 11 import java.util.ArrayList; 12 import java.util.Collection; 13 import java.util.Collections; 14 import java.util.List; 15 import java.util.Map; 16 import java.util.WeakHashMap; 17 import java.util.concurrent.TimeUnit; 18 import org.robolectric.RoboSettings; 19 import org.robolectric.RuntimeEnvironment; 20 import org.robolectric.annotation.Implementation; 21 import org.robolectric.annotation.Implements; 22 import org.robolectric.annotation.LooperMode; 23 import org.robolectric.annotation.RealObject; 24 import org.robolectric.annotation.Resetter; 25 import org.robolectric.config.ConfigurationRegistry; 26 import org.robolectric.shadow.api.Shadow; 27 import org.robolectric.util.Scheduler; 28 29 /** 30 * The shadow Looper implementation for {@link LooperMode.Mode.LEGACY}. 31 * 32 * <p>Robolectric enqueues posted {@link Runnable}s to be run (on this thread) later. {@code 33 * Runnable}s that are scheduled to run immediately can be triggered by calling {@link #idle()}. 34 * 35 * @see ShadowMessageQueue 36 */ 37 @Implements(value = Looper.class, isInAndroidSdk = false) 38 @SuppressWarnings("SynchronizeOnNonFinalField") 39 public class ShadowLegacyLooper extends ShadowLooper { 40 41 // Replaced SoftThreadLocal with a WeakHashMap, because ThreadLocal make it impossible to access 42 // their contents from other threads, but we need to be able to access the loopers for all 43 // threads so that we can shut them down when resetThreadLoopers() 44 // is called. This also allows us to implement the useful getLooperForThread() method. 45 // Note that the main looper is handled differently and is not put in this hash, because we need 46 // to be able to "switch" the thread that the main looper is associated with. 47 private static Map<Thread, Looper> loopingLoopers = 48 Collections.synchronizedMap(new WeakHashMap<Thread, Looper>()); 49 50 private static Looper mainLooper; 51 52 private static Scheduler backgroundScheduler; 53 54 private @RealObject Looper realObject; 55 56 boolean quit; 57 58 @Resetter resetThreadLoopers()59 public static synchronized void resetThreadLoopers() { 60 // do not use looperMode() here, because its cached value might already have been reset 61 if (ConfigurationRegistry.get(LooperMode.Mode.class) == LooperMode.Mode.PAUSED) { 62 // ignore if realistic looper 63 return; 64 } 65 synchronized (loopingLoopers) { 66 for (Looper looper : loopingLoopers.values()) { 67 synchronized (looper) { 68 if (!shadowOf(looper).quit) { 69 looper.quit(); 70 } else { 71 // Reset the schedulers of all loopers. This prevents un-run tasks queued up in static 72 // background handlers from leaking to subsequent tests. 73 shadowOf(looper).getScheduler().reset(); 74 shadowOf(looper.getQueue()).reset(); 75 } 76 } 77 } 78 } 79 // Because resetStaticState() is called by AndroidTestEnvironment on startup before 80 // prepareMainLooper() is called, this might be null on that occasion. 81 if (mainLooper != null) { 82 shadowOf(mainLooper).reset(); 83 } 84 } 85 getBackgroundThreadScheduler()86 static synchronized Scheduler getBackgroundThreadScheduler() { 87 return backgroundScheduler; 88 } 89 90 /** Internal API to initialize background thread scheduler from AndroidTestEnvironment. */ internalInitializeBackgroundThreadScheduler()91 public static void internalInitializeBackgroundThreadScheduler() { 92 backgroundScheduler = 93 RoboSettings.isUseGlobalScheduler() 94 ? RuntimeEnvironment.getMasterScheduler() 95 : new Scheduler(); 96 } 97 98 @Implementation __constructor__(boolean quitAllowed)99 protected void __constructor__(boolean quitAllowed) { 100 invokeConstructor(Looper.class, realObject, from(boolean.class, quitAllowed)); 101 if (isMainThread()) { 102 mainLooper = realObject; 103 } else { 104 loopingLoopers.put(Thread.currentThread(), realObject); 105 } 106 resetScheduler(); 107 } 108 109 @Implementation getMainLooper()110 protected static Looper getMainLooper() { 111 return mainLooper; 112 } 113 114 @Implementation myLooper()115 protected static Looper myLooper() { 116 return getLooperForThread(Thread.currentThread()); 117 } 118 119 @Implementation loop()120 protected static void loop() { 121 shadowOf(Looper.myLooper()).doLoop(); 122 } 123 doLoop()124 private void doLoop() { 125 if (realObject != Looper.getMainLooper()) { 126 synchronized (realObject) { 127 while (!quit) { 128 try { 129 realObject.wait(); 130 } catch (InterruptedException ignore) { 131 } 132 } 133 } 134 } 135 } 136 137 @Implementation quit()138 protected void quit() { 139 if (realObject == Looper.getMainLooper()) { 140 throw new RuntimeException("Main thread not allowed to quit"); 141 } 142 quitUnchecked(); 143 } 144 145 @Implementation(minSdk = JELLY_BEAN_MR2) quitSafely()146 protected void quitSafely() { 147 quit(); 148 } 149 150 @Override quitUnchecked()151 public void quitUnchecked() { 152 synchronized (realObject) { 153 quit = true; 154 realObject.notifyAll(); 155 getScheduler().reset(); 156 shadowOf(realObject.getQueue()).reset(); 157 } 158 } 159 160 @Override hasQuit()161 public boolean hasQuit() { 162 synchronized (realObject) { 163 return quit; 164 } 165 } 166 getLooperForThread(Thread thread)167 public static Looper getLooperForThread(Thread thread) { 168 return isMainThread(thread) ? mainLooper : loopingLoopers.get(thread); 169 } 170 171 /** Return loopers for all threads including main thread. */ getLoopers()172 protected static Collection<Looper> getLoopers() { 173 List<Looper> loopers = new ArrayList<>(loopingLoopers.values()); 174 loopers.add(mainLooper); 175 return Collections.unmodifiableCollection(loopers); 176 } 177 178 @Override idle()179 public void idle() { 180 idle(0, TimeUnit.MILLISECONDS); 181 } 182 183 @Override idleFor(long time, TimeUnit timeUnit)184 public void idleFor(long time, TimeUnit timeUnit) { 185 getScheduler().advanceBy(time, timeUnit); 186 } 187 188 @Override isIdle()189 public boolean isIdle() { 190 return !getScheduler().areAnyRunnable(); 191 } 192 193 @Override idleIfPaused()194 public void idleIfPaused() { 195 // ignore 196 } 197 198 @Override idleConstantly(boolean shouldIdleConstantly)199 public void idleConstantly(boolean shouldIdleConstantly) { 200 getScheduler().idleConstantly(shouldIdleConstantly); 201 } 202 203 @Override runToEndOfTasks()204 public void runToEndOfTasks() { 205 getScheduler().advanceToLastPostedRunnable(); 206 } 207 208 @Override runToNextTask()209 public void runToNextTask() { 210 getScheduler().advanceToNextPostedRunnable(); 211 } 212 213 @Override runOneTask()214 public void runOneTask() { 215 getScheduler().runOneTask(); 216 } 217 218 /** 219 * Enqueue a task to be run later. 220 * 221 * @param runnable the task to be run 222 * @param delayMillis how many milliseconds into the (virtual) future to run it 223 * @return true if the runnable is enqueued 224 * @see android.os.Handler#postDelayed(Runnable,long) 225 * @deprecated Use a {@link android.os.Handler} instance to post to a looper. 226 */ 227 @Override 228 @Deprecated post(Runnable runnable, long delayMillis)229 public boolean post(Runnable runnable, long delayMillis) { 230 if (!quit) { 231 getScheduler().postDelayed(runnable, delayMillis, TimeUnit.MILLISECONDS); 232 return true; 233 } else { 234 return false; 235 } 236 } 237 238 /** 239 * Enqueue a task to be run ahead of all other delayed tasks. 240 * 241 * @param runnable the task to be run 242 * @return true if the runnable is enqueued 243 * @see android.os.Handler#postAtFrontOfQueue(Runnable) 244 * @deprecated Use a {@link android.os.Handler} instance to post to a looper. 245 */ 246 @Override 247 @Deprecated postAtFrontOfQueue(Runnable runnable)248 public boolean postAtFrontOfQueue(Runnable runnable) { 249 if (!quit) { 250 getScheduler().postAtFrontOfQueue(runnable); 251 return true; 252 } else { 253 return false; 254 } 255 } 256 257 @Override pause()258 public void pause() { 259 getScheduler().pause(); 260 } 261 262 @Override getNextScheduledTaskTime()263 public Duration getNextScheduledTaskTime() { 264 return getScheduler().getNextScheduledTaskTime(); 265 } 266 267 @Override getLastScheduledTaskTime()268 public Duration getLastScheduledTaskTime() { 269 return getScheduler().getLastScheduledTaskTime(); 270 } 271 272 @Override unPause()273 public void unPause() { 274 getScheduler().unPause(); 275 } 276 277 @Override isPaused()278 public boolean isPaused() { 279 return getScheduler().isPaused(); 280 } 281 282 @Override setPaused(boolean shouldPause)283 public boolean setPaused(boolean shouldPause) { 284 boolean wasPaused = isPaused(); 285 if (shouldPause) { 286 pause(); 287 } else { 288 unPause(); 289 } 290 return wasPaused; 291 } 292 293 @Override resetScheduler()294 public void resetScheduler() { 295 ShadowMessageQueue shadowMessageQueue = shadowOf(realObject.getQueue()); 296 if (realObject == Looper.getMainLooper() || RoboSettings.isUseGlobalScheduler()) { 297 shadowMessageQueue.setScheduler(RuntimeEnvironment.getMasterScheduler()); 298 } else { 299 shadowMessageQueue.setScheduler(new Scheduler()); 300 } 301 } 302 303 /** Causes all enqueued tasks to be discarded, and pause state to be reset */ 304 @Override reset()305 public void reset() { 306 shadowOf(realObject.getQueue()).reset(); 307 resetScheduler(); 308 309 quit = false; 310 } 311 312 /** 313 * Returns the {@link org.robolectric.util.Scheduler} that is being used to manage the enqueued 314 * tasks. This scheduler is managed by the Looper's associated queue. 315 * 316 * @return the {@link org.robolectric.util.Scheduler} that is being used to manage the enqueued 317 * tasks. 318 */ 319 @Override getScheduler()320 public Scheduler getScheduler() { 321 return shadowOf(realObject.getQueue()).getScheduler(); 322 } 323 324 @Override runPaused(Runnable r)325 public void runPaused(Runnable r) { 326 boolean wasPaused = setPaused(true); 327 try { 328 r.run(); 329 } finally { 330 if (!wasPaused) unPause(); 331 } 332 } 333 shadowOf(Looper looper)334 private static ShadowLegacyLooper shadowOf(Looper looper) { 335 return Shadow.extract(looper); 336 } 337 shadowOf(MessageQueue mq)338 private static ShadowMessageQueue shadowOf(MessageQueue mq) { 339 return Shadow.extract(mq); 340 } 341 } 342