• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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