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