1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; 4 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2; 5 import static android.os.Build.VERSION_CODES.KITKAT; 6 import static android.os.Build.VERSION_CODES.KITKAT_WATCH; 7 import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1; 8 import static android.os.Build.VERSION_CODES.M; 9 import static com.google.common.base.Preconditions.checkState; 10 import static org.robolectric.shadow.api.Shadow.invokeConstructor; 11 import static org.robolectric.util.ReflectionHelpers.ClassParameter.from; 12 import static org.robolectric.util.reflector.Reflector.reflector; 13 14 import android.os.Looper; 15 import android.os.Message; 16 import android.os.MessageQueue; 17 import android.os.MessageQueue.IdleHandler; 18 import android.os.SystemClock; 19 import android.util.Log; 20 import androidx.annotation.VisibleForTesting; 21 import com.google.common.base.Predicate; 22 import java.time.Duration; 23 import java.util.ArrayList; 24 import org.robolectric.RuntimeEnvironment; 25 import org.robolectric.annotation.Implementation; 26 import org.robolectric.annotation.Implements; 27 import org.robolectric.annotation.LooperMode; 28 import org.robolectric.annotation.RealObject; 29 import org.robolectric.res.android.NativeObjRegistry; 30 import org.robolectric.shadow.api.Shadow; 31 import org.robolectric.util.ReflectionHelpers; 32 import org.robolectric.util.Scheduler; 33 import org.robolectric.util.reflector.Accessor; 34 import org.robolectric.util.reflector.Direct; 35 import org.robolectric.util.reflector.ForType; 36 37 /** 38 * The shadow {@link} MessageQueue} for {@link LooperMode.Mode.PAUSED} 39 * 40 * <p>This class should not be referenced directly. Use {@link ShadowMessageQueue} instead. 41 */ 42 @SuppressWarnings("SynchronizeOnNonFinalField") 43 @Implements(value = MessageQueue.class, isInAndroidSdk = false, looseSignatures = true) 44 public class ShadowPausedMessageQueue extends ShadowMessageQueue { 45 46 @RealObject private MessageQueue realQueue; 47 48 // just use this class as the native object 49 private static NativeObjRegistry<ShadowPausedMessageQueue> nativeQueueRegistry = 50 new NativeObjRegistry<ShadowPausedMessageQueue>(ShadowPausedMessageQueue.class); 51 private boolean isPolling = false; 52 private ShadowPausedSystemClock.Listener clockListener; 53 private Exception uncaughtException = null; 54 55 // shadow constructor instead of nativeInit because nativeInit signature has changed across SDK 56 // versions 57 @Implementation __constructor__(boolean quitAllowed)58 protected void __constructor__(boolean quitAllowed) { 59 invokeConstructor(MessageQueue.class, realQueue, from(boolean.class, quitAllowed)); 60 int ptr = (int) nativeQueueRegistry.register(this); 61 reflector(MessageQueueReflector.class, realQueue).setPtr(ptr); 62 clockListener = 63 () -> { 64 synchronized (realQueue) { 65 // only wake up the Looper thread if queue is non empty to reduce contention if many 66 // Looper threads are active 67 if (getMessages() != null) { 68 nativeWake(ptr); 69 } 70 } 71 }; 72 ShadowPausedSystemClock.addStaticListener(clockListener); 73 } 74 75 @Implementation(maxSdk = JELLY_BEAN_MR1) nativeDestroy()76 protected void nativeDestroy() { 77 nativeDestroy(reflector(MessageQueueReflector.class, realQueue).getPtr()); 78 } 79 80 @Implementation(minSdk = JELLY_BEAN_MR2, maxSdk = KITKAT) nativeDestroy(int ptr)81 protected static void nativeDestroy(int ptr) { 82 nativeDestroy((long) ptr); 83 } 84 85 @Implementation(minSdk = KITKAT_WATCH) nativeDestroy(long ptr)86 protected static void nativeDestroy(long ptr) { 87 ShadowPausedMessageQueue q = nativeQueueRegistry.unregister(ptr); 88 ShadowPausedSystemClock.removeListener(q.clockListener); 89 } 90 91 @Implementation(maxSdk = JELLY_BEAN_MR1) nativePollOnce(int ptr, int timeoutMillis)92 protected void nativePollOnce(int ptr, int timeoutMillis) { 93 nativePollOnce((long) ptr, timeoutMillis); 94 } 95 96 // use the generic Object parameter types here, to avoid conflicts with the non-static 97 // nativePollOnce 98 @Implementation(minSdk = JELLY_BEAN_MR2, maxSdk = LOLLIPOP_MR1) nativePollOnce(Object ptr, Object timeoutMillis)99 protected static void nativePollOnce(Object ptr, Object timeoutMillis) { 100 long ptrLong = getLong(ptr); 101 nativeQueueRegistry.getNativeObject(ptrLong).nativePollOnce(ptrLong, (int) timeoutMillis); 102 } 103 104 @Implementation(minSdk = M) nativePollOnce(long ptr, int timeoutMillis)105 protected void nativePollOnce(long ptr, int timeoutMillis) { 106 if (timeoutMillis == 0) { 107 return; 108 } 109 synchronized (realQueue) { 110 // only block if queue is empty 111 // ignore timeout since clock is not advancing. ClockListener will notify when clock advances 112 while (isIdle() && !isQuitting()) { 113 isPolling = true; 114 try { 115 realQueue.wait(); 116 } catch (InterruptedException e) { 117 // ignore 118 } 119 } 120 isPolling = false; 121 } 122 } 123 124 /** 125 * Polls the message queue waiting until a message is posted to the head of the queue. This will 126 * suspend the thread until a new message becomes available. Returns immediately if the queue is 127 * not idle. There's no guarantee that the message queue will not still be idle when returning, 128 * but if the message queue becomes not idle it will return immediately. 129 * 130 * <p>See {@link ShadowPausedLooper#poll(long)} for more information. 131 * 132 * @param timeout Timeout in milliseconds, the maximum time to wait before returning, or 0 to wait 133 * indefinitely, 134 */ poll(long timeout)135 void poll(long timeout) { 136 checkState(Looper.myLooper() == Looper.getMainLooper() && Looper.myQueue() == realQueue); 137 // Message queue typically expects the looper to loop calling next() which returns current 138 // messages from the head of the queue. If no messages are current it will mark itself blocked 139 // and call nativePollOnce (see above) which suspends the thread until the next message's time. 140 // When messages are posted to the queue, if a new message is posted to the head and the queue 141 // is marked as blocked, then the enqueue function will notify and resume next(), allowing it 142 // return the next message. To simulate this behavior check if the queue is idle and if it is 143 // mark the queue as blocked and wait on a new message. 144 synchronized (realQueue) { 145 if (isIdle()) { 146 ReflectionHelpers.setField(realQueue, "mBlocked", true); 147 try { 148 realQueue.wait(timeout); 149 } catch (InterruptedException ignored) { 150 // Fall through and unblock with no messages. 151 } finally { 152 ReflectionHelpers.setField(realQueue, "mBlocked", false); 153 } 154 } 155 } 156 } 157 158 @Implementation(maxSdk = JELLY_BEAN_MR1) nativeWake(int ptr)159 protected void nativeWake(int ptr) { 160 synchronized (realQueue) { 161 realQueue.notifyAll(); 162 } 163 } 164 165 // use the generic Object parameter types here, to avoid conflicts with the non-static 166 // nativeWake 167 @Implementation(minSdk = JELLY_BEAN_MR2, maxSdk = KITKAT) nativeWake(Object ptr)168 protected static void nativeWake(Object ptr) { 169 // JELLY_BEAN_MR2 has a bug where nativeWake can get called when pointer has already been 170 // destroyed. See here where nativeWake is called outside the synchronized block 171 // https://android.googlesource.com/platform/frameworks/base/+/refs/heads/jb-mr2-release/core/java/android/os/MessageQueue.java#239 172 // So check to see if native object exists first 173 ShadowPausedMessageQueue q = nativeQueueRegistry.peekNativeObject(getLong(ptr)); 174 if (q != null) { 175 q.nativeWake(getInt(ptr)); 176 } 177 } 178 179 @Implementation(minSdk = KITKAT_WATCH) nativeWake(long ptr)180 protected static void nativeWake(long ptr) { 181 nativeQueueRegistry.getNativeObject(ptr).nativeWake((int) ptr); 182 } 183 184 @Implementation(minSdk = M) nativeIsPolling(long ptr)185 protected static boolean nativeIsPolling(long ptr) { 186 return nativeQueueRegistry.getNativeObject(ptr).isPolling; 187 } 188 189 /** Exposes the API23+_isIdle method to older platforms */ 190 @Implementation(minSdk = 23) isIdle()191 public boolean isIdle() { 192 synchronized (realQueue) { 193 Message msg = peekNextExecutableMessage(); 194 if (msg == null) { 195 return true; 196 } 197 198 long now = SystemClock.uptimeMillis(); 199 long when = shadowOfMsg(msg).getWhen(); 200 return now < when; 201 } 202 } 203 peekNextExecutableMessage()204 Message peekNextExecutableMessage() { 205 MessageQueueReflector internalQueue = reflector(MessageQueueReflector.class, realQueue); 206 Message msg = internalQueue.getMessages(); 207 208 if (msg != null && shadowOfMsg(msg).getTarget() == null) { 209 // Stalled by a barrier. Find the next asynchronous message in the queue. 210 do { 211 msg = shadowOfMsg(msg).internalGetNext(); 212 } while (msg != null && !msg.isAsynchronous()); 213 } 214 215 return msg; 216 } 217 getNext()218 Message getNext() { 219 return reflector(MessageQueueReflector.class, realQueue).next(); 220 } 221 isQuitAllowed()222 boolean isQuitAllowed() { 223 return reflector(MessageQueueReflector.class, realQueue).getQuitAllowed(); 224 } 225 226 @VisibleForTesting doEnqueueMessage(Message msg, long when)227 void doEnqueueMessage(Message msg, long when) { 228 enqueueMessage(msg, when); 229 } 230 231 @Implementation enqueueMessage(Message msg, long when)232 protected boolean enqueueMessage(Message msg, long when) { 233 synchronized (realQueue) { 234 if (uncaughtException != null) { 235 // looper thread has died 236 IllegalStateException e = 237 new IllegalStateException( 238 msg.getTarget() 239 + " sending message to a Looper thread that has died due to an uncaught" 240 + " exception", 241 uncaughtException); 242 Log.w("ShadowPausedMessageQueue", e); 243 msg.recycle(); 244 throw e; 245 } 246 return reflector(MessageQueueReflector.class, realQueue).enqueueMessage(msg, when); 247 } 248 } 249 getMessages()250 Message getMessages() { 251 return reflector(MessageQueueReflector.class, realQueue).getMessages(); 252 } 253 254 @Implementation(minSdk = M) isPolling()255 protected boolean isPolling() { 256 synchronized (realQueue) { 257 return isPolling; 258 } 259 } 260 261 @Implementation(maxSdk = JELLY_BEAN_MR1) quit()262 protected void quit() { 263 if (RuntimeEnvironment.getApiLevel() >= JELLY_BEAN_MR2) { 264 reflector(MessageQueueReflector.class, realQueue).quit(false); 265 } else { 266 reflector(MessageQueueReflector.class, realQueue).quit(); 267 } 268 } 269 270 @Implementation(minSdk = JELLY_BEAN_MR2) quit(boolean allowed)271 protected void quit(boolean allowed) { 272 reflector(MessageQueueReflector.class, realQueue).quit(allowed); 273 ShadowPausedSystemClock.removeListener(clockListener); 274 } 275 isQuitting()276 private boolean isQuitting() { 277 if (RuntimeEnvironment.getApiLevel() >= KITKAT) { 278 return reflector(MessageQueueReflector.class, realQueue).getQuitting(); 279 } else { 280 return reflector(MessageQueueReflector.class, realQueue).getQuiting(); 281 } 282 } 283 getLong(Object intOrLongObj)284 private static long getLong(Object intOrLongObj) { 285 if (intOrLongObj instanceof Long) { 286 return (long) intOrLongObj; 287 } else { 288 Integer intObj = (Integer) intOrLongObj; 289 return intObj.longValue(); 290 } 291 } 292 getInt(Object intOrLongObj)293 private static int getInt(Object intOrLongObj) { 294 if (intOrLongObj instanceof Integer) { 295 return (int) intOrLongObj; 296 } else { 297 Long longObj = (Long) intOrLongObj; 298 return longObj.intValue(); 299 } 300 } 301 getNextScheduledTaskTime()302 Duration getNextScheduledTaskTime() { 303 Message next = peekNextExecutableMessage(); 304 305 if (next == null) { 306 return Duration.ZERO; 307 } 308 return Duration.ofMillis(convertWhenToScheduledTime(shadowOfMsg(next).getWhen())); 309 } 310 getLastScheduledTaskTime()311 Duration getLastScheduledTaskTime() { 312 long when = 0; 313 synchronized (realQueue) { 314 Message next = getMessages(); 315 if (next == null) { 316 return Duration.ZERO; 317 } 318 while (next != null) { 319 if (next.getTarget() != null) { 320 when = shadowOfMsg(next).getWhen(); 321 } 322 next = shadowOfMsg(next).internalGetNext(); 323 } 324 } 325 return Duration.ofMillis(convertWhenToScheduledTime(when)); 326 } 327 convertWhenToScheduledTime(long when)328 private static long convertWhenToScheduledTime(long when) { 329 // in some situations, when can be 0 or less than uptimeMillis. Always floor it to at least 330 // convertWhenToUptime 331 if (when < SystemClock.uptimeMillis()) { 332 when = SystemClock.uptimeMillis(); 333 } 334 return when; 335 } 336 337 /** 338 * Internal method to get the number of entries in the MessageQueue. 339 * 340 * <p>Do not use, will likely be removed in a future release. 341 */ internalGetSize()342 public int internalGetSize() { 343 int count = 0; 344 synchronized (realQueue) { 345 Message next = getMessages(); 346 while (next != null) { 347 if (next.getTarget() != null) { 348 count++; 349 } 350 next = shadowOfMsg(next).internalGetNext(); 351 } 352 } 353 return count; 354 } 355 356 /** 357 * Returns the message at the head of the queue immediately, regardless of its scheduled time. 358 * Compare to {@link #getNext()} which will only return the next message if the system clock is 359 * advanced to its scheduled time. 360 */ getNextIgnoringWhen()361 Message getNextIgnoringWhen() { 362 synchronized (realQueue) { 363 Message prev = null; 364 Message msg = getMessages(); 365 // Head is blocked on synchronization barrier, find next asynchronous message. 366 if (msg != null && msg.getTarget() == null) { 367 do { 368 prev = msg; 369 msg = shadowOfMsg(msg).internalGetNext(); 370 } while (msg != null && !msg.isAsynchronous()); 371 } 372 if (msg != null) { 373 Message next = shadowOfMsg(msg).internalGetNext(); 374 if (prev == null) { 375 reflector(MessageQueueReflector.class, realQueue).setMessages(next); 376 } else { 377 ReflectionHelpers.setField(prev, "next", next); 378 } 379 } 380 return msg; 381 } 382 } 383 384 // TODO: reconsider exposing this as a public API. Only ShadowPausedLooper needs to access this, 385 // so it should be package private 386 @Override reset()387 public void reset() { 388 MessageQueueReflector msgQueue = reflector(MessageQueueReflector.class, realQueue); 389 msgQueue.setMessages(null); 390 msgQueue.setIdleHandlers(new ArrayList<>()); 391 msgQueue.setNextBarrierToken(0); 392 setUncaughtException(null); 393 } 394 shadowOfMsg(Message head)395 private static ShadowPausedMessage shadowOfMsg(Message head) { 396 return Shadow.extract(head); 397 } 398 399 @Override getScheduler()400 public Scheduler getScheduler() { 401 throw new UnsupportedOperationException("Not supported in PAUSED LooperMode."); 402 } 403 404 @Override setScheduler(Scheduler scheduler)405 public void setScheduler(Scheduler scheduler) { 406 throw new UnsupportedOperationException("Not supported in PAUSED LooperMode."); 407 } 408 409 // intentionally do not support direct access to MessageQueue internals 410 411 @Override getHead()412 public Message getHead() { 413 throw new UnsupportedOperationException("Not supported in PAUSED LooperMode."); 414 } 415 416 @Override setHead(Message msg)417 public void setHead(Message msg) { 418 throw new UnsupportedOperationException("Not supported in PAUSED LooperMode."); 419 } 420 421 /** 422 * Retrieves a copy of the current list of idle handlers. Idle handlers are read with 423 * synchronization on the real queue. 424 */ getIdleHandlersCopy()425 ArrayList<IdleHandler> getIdleHandlersCopy() { 426 synchronized (realQueue) { 427 return new ArrayList<>(reflector(MessageQueueReflector.class, realQueue).getIdleHandlers()); 428 } 429 } 430 431 /** 432 * Called when an uncaught exception occurred in this message queue's Looper thread. 433 * 434 * <p>In real android, by default an exception handler is installed which kills the entire process 435 * when an uncaught exception occurs. We don't want to do this in robolectric to isolate tests, so 436 * instead an uncaught exception puts the message queue into an error state, where any future 437 * interaction will rethrow the exception. 438 */ setUncaughtException(Exception e)439 void setUncaughtException(Exception e) { 440 synchronized (realQueue) { 441 this.uncaughtException = e; 442 } 443 } 444 checkQueueState()445 void checkQueueState() { 446 synchronized (realQueue) { 447 if (uncaughtException != null) { 448 throw new IllegalStateException( 449 "Looper thread that has died due to an uncaught exception", uncaughtException); 450 } 451 } 452 } 453 454 /** 455 * Remove all messages from queue 456 * 457 * @param msgProcessor a callback to apply to each mesg 458 */ drainQueue(Predicate<Runnable> msgProcessor)459 void drainQueue(Predicate<Runnable> msgProcessor) { 460 synchronized (realQueue) { 461 Message msg = getMessages(); 462 while (msg != null) { 463 boolean unused = msgProcessor.apply(msg.getCallback()); 464 Message next = shadowOfMsg(msg).internalGetNext(); 465 shadowOfMsg(msg).recycleUnchecked(); 466 msg = next; 467 } 468 reflector(MessageQueueReflector.class, realQueue).setMessages(null); 469 } 470 } 471 472 /** Accessor interface for {@link MessageQueue}'s internals. */ 473 @ForType(MessageQueue.class) 474 private interface MessageQueueReflector { 475 @Direct enqueueMessage(Message msg, long when)476 boolean enqueueMessage(Message msg, long when); 477 next()478 Message next(); 479 480 @Accessor("mMessages") setMessages(Message msg)481 void setMessages(Message msg); 482 483 @Accessor("mMessages") getMessages()484 Message getMessages(); 485 486 @Accessor("mIdleHandlers") setIdleHandlers(ArrayList<IdleHandler> list)487 void setIdleHandlers(ArrayList<IdleHandler> list); 488 489 @Accessor("mIdleHandlers") getIdleHandlers()490 ArrayList<IdleHandler> getIdleHandlers(); 491 492 @Accessor("mNextBarrierToken") setNextBarrierToken(int token)493 void setNextBarrierToken(int token); 494 495 @Accessor("mQuitAllowed") getQuitAllowed()496 boolean getQuitAllowed(); 497 498 @Accessor("mPtr") setPtr(int ptr)499 void setPtr(int ptr); 500 501 @Accessor("mPtr") getPtr()502 int getPtr(); 503 504 // for APIs < JELLYBEAN_MR2 505 @Direct quit()506 void quit(); 507 508 @Direct quit(boolean b)509 void quit(boolean b); 510 511 // for APIs < KITKAT 512 @Accessor("mQuiting") getQuiting()513 boolean getQuiting(); 514 515 @Accessor("mQuitting") getQuitting()516 boolean getQuitting(); 517 } 518 } 519