Home | History | Annotate | Download | only in shadows
      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.util.Collections;
     11 import java.util.Map;
     12 import java.util.WeakHashMap;
     13 import java.util.concurrent.TimeUnit;
     14 import org.robolectric.RoboSettings;
     15 import org.robolectric.RuntimeEnvironment;
     16 import org.robolectric.annotation.Implementation;
     17 import org.robolectric.annotation.Implements;
     18 import org.robolectric.annotation.RealObject;
     19 import org.robolectric.annotation.Resetter;
     20 import org.robolectric.shadow.api.Shadow;
     21 import org.robolectric.util.Scheduler;
     22 
     23 /**
     24  * Robolectric enqueues posted {@link Runnable}s to be run
     25  * (on this thread) later. {@code Runnable}s that are scheduled to run immediately can be
     26  * triggered by calling {@link #idle()}.
     27  *
     28  * @see ShadowMessageQueue
     29  */
     30 @Implements(Looper.class)
     31 @SuppressWarnings("SynchronizeOnNonFinalField")
     32 public class ShadowLooper {
     33 
     34   // Replaced SoftThreadLocal with a WeakHashMap, because ThreadLocal make it impossible to access their contents from other
     35   // threads, but we need to be able to access the loopers for all threads so that we can shut them down when resetThreadLoopers()
     36   // is called. This also allows us to implement the useful getLooperForThread() method.
     37   // Note that the main looper is handled differently and is not put in this hash, because we need to be able to
     38   // "switch" the thread that the main looper is associated with.
     39   private static Map<Thread, Looper> loopingLoopers = Collections.synchronizedMap(new WeakHashMap<Thread, Looper>());
     40 
     41   private static Looper mainLooper;
     42 
     43   private @RealObject Looper realObject;
     44 
     45   boolean quit;
     46 
     47   @Resetter
     48   public static synchronized void resetThreadLoopers() {
     49     // Blech. We need to keep the main looper because somebody might refer to it in a static
     50     // field. The other loopers need to be wrapped in WeakReferences so that they are not prevented from
     51     // being garbage collected.
     52     if (!isMainThread()) {
     53       throw new IllegalStateException("you should only be calling this from the main thread!");
     54     }
     55     synchronized (loopingLoopers) {
     56       for (Looper looper : loopingLoopers.values()) {
     57         synchronized (looper) {
     58           if (!shadowOf(looper).quit) {
     59             looper.quit();
     60           } else {
     61             // Reset the schedulers of all loopers. This prevents un-run tasks queued up in static
     62             // background handlers from leaking to subsequent tests.
     63             shadowOf(looper).getScheduler().reset();
     64             shadowOf(looper.getQueue()).reset();
     65           }
     66         }
     67       }
     68     }
     69     // Because resetStaticState() is called by ParallelUniverse on startup before prepareMainLooper() is
     70     // called, this might be null on that occasion.
     71     if (mainLooper != null) {
     72       shadowOf(mainLooper).reset();
     73     }
     74   }
     75 
     76   @Implementation
     77   protected void __constructor__(boolean quitAllowed) {
     78     invokeConstructor(Looper.class, realObject, from(boolean.class, quitAllowed));
     79     if (isMainThread()) {
     80       mainLooper = realObject;
     81     } else {
     82       loopingLoopers.put(Thread.currentThread(), realObject);
     83     }
     84     resetScheduler();
     85   }
     86 
     87   @Implementation
     88   protected static Looper getMainLooper() {
     89     return mainLooper;
     90   }
     91 
     92   @Implementation
     93   protected static Looper myLooper() {
     94     return getLooperForThread(Thread.currentThread());
     95   }
     96 
     97   @Implementation
     98   protected static void loop() {
     99     shadowOf(Looper.myLooper()).doLoop();
    100   }
    101 
    102   private void doLoop() {
    103     if (realObject != Looper.getMainLooper()) {
    104       synchronized (realObject) {
    105         while (!quit) {
    106           try {
    107             realObject.wait();
    108           } catch (InterruptedException ignore) {
    109           }
    110         }
    111       }
    112     }
    113   }
    114 
    115   @Implementation
    116   protected void quit() {
    117     if (realObject == Looper.getMainLooper()) throw new RuntimeException("Main thread not allowed to quit");
    118     quitUnchecked();
    119   }
    120 
    121   @Implementation(minSdk = JELLY_BEAN_MR2)
    122   protected void quitSafely() {
    123     quit();
    124   }
    125 
    126   public void quitUnchecked() {
    127     synchronized (realObject) {
    128       quit = true;
    129       realObject.notifyAll();
    130       getScheduler().reset();
    131       shadowOf(realObject.getQueue()).reset();
    132     }
    133   }
    134 
    135   public boolean hasQuit() {
    136     synchronized (realObject) {
    137       return quit;
    138     }
    139   }
    140 
    141   /** @deprecated Use `shadowOf({@link Looper#getMainLooper()})` instead. */
    142   @Deprecated
    143   public static ShadowLooper getShadowMainLooper() {
    144     return shadowOf(Looper.getMainLooper());
    145   }
    146 
    147   public static Looper getLooperForThread(Thread thread) {
    148     return isMainThread(thread) ? mainLooper : loopingLoopers.get(thread);
    149   }
    150 
    151   /**
    152    * Pauses execution of tasks posted to the ShadowLooper. This means that during tests, tasks sent
    153    * to the looper will not execute immediately, but will be queued in a way that is similar to how
    154    * a real looper works. These queued tasks must be executed explicitly by calling {@link
    155    * #runToEndOftasks} or a similar method, otherwise they will not run at all before your test
    156    * ends.
    157    *
    158    * @param looper the looper to pause
    159    */
    160   public static void pauseLooper(Looper looper) {
    161     shadowOf(looper).pause();
    162   }
    163 
    164   /**
    165    * Puts the shadow looper in an "unpaused" state (this is the default state). This means that
    166    * during tests, tasks sent to the looper will execute inline, immediately, on the calling (main)
    167    * thread instead of being queued, in a way similar to how Guava's "DirectExecutorService" works.
    168    * This is likely not to be what you want: it will cause code to be potentially executed in a
    169    * different order than how it would execute on the device, and if you are using certain Android
    170    * APIs (such as view animations) that are non-reentrant, they may not work at all or do
    171    * unpredictable things. For more information, see <a
    172    * href="https://github.com/robolectric/robolectric/issues/3369">this discussion</a>.
    173    *
    174    * @param looper the looper to pause
    175    */
    176   public static void unPauseLooper(Looper looper) {
    177     shadowOf(looper).unPause();
    178   }
    179 
    180   /**
    181    * Puts the main ShadowLooper in an "paused" state.
    182    *
    183    * @see #pauseLooper
    184    */
    185   public static void pauseMainLooper() {
    186     getShadowMainLooper().pause();
    187   }
    188 
    189   /**
    190    * Puts the main ShadowLooper in an "unpaused" state.
    191    *
    192    * @see #unPauseLooper
    193    */
    194   public static void unPauseMainLooper() {
    195     getShadowMainLooper().unPause();
    196   }
    197 
    198   public static void idleMainLooper() {
    199     getShadowMainLooper().idle();
    200   }
    201 
    202   /** @deprecated Use {@link #idleMainLooper(long, TimeUnit)}. */
    203   @Deprecated
    204   public static void idleMainLooper(long interval) {
    205     idleMainLooper(interval, TimeUnit.MILLISECONDS);
    206   }
    207 
    208   public static void idleMainLooper(long amount, TimeUnit unit) {
    209     getShadowMainLooper().idle(amount, unit);
    210   }
    211 
    212   public static void idleMainLooperConstantly(boolean shouldIdleConstantly) {
    213     getShadowMainLooper().idleConstantly(shouldIdleConstantly);
    214   }
    215 
    216   public static void runMainLooperOneTask() {
    217     getShadowMainLooper().runOneTask();
    218   }
    219 
    220   public static void runMainLooperToNextTask() {
    221     getShadowMainLooper().runToNextTask();
    222   }
    223 
    224   /**
    225    * Runs any immediately runnable tasks previously queued on the UI thread,
    226    * e.g. by {@link android.app.Activity#runOnUiThread(Runnable)} or {@link android.os.AsyncTask#onPostExecute(Object)}.
    227    *
    228    * **Note:** calling this method does not pause or un-pause the scheduler.
    229    *
    230    * @see #runUiThreadTasksIncludingDelayedTasks
    231    */
    232   public static void runUiThreadTasks() {
    233     getShadowMainLooper().idle();
    234   }
    235 
    236   /**
    237    * Runs all runnable tasks (pending and future) that have been queued on the UI thread. Such tasks may be queued by
    238    * e.g. {@link android.app.Activity#runOnUiThread(Runnable)} or {@link android.os.AsyncTask#onPostExecute(Object)}.
    239    *
    240    * **Note:** calling this method does not pause or un-pause the scheduler, however the clock is advanced as
    241    * future tasks are run.
    242    *
    243    * @see #runUiThreadTasks
    244    */
    245   public static void runUiThreadTasksIncludingDelayedTasks() {
    246     getShadowMainLooper().runToEndOfTasks();
    247   }
    248 
    249   /**
    250    * Causes {@link Runnable}s that have been scheduled to run immediately to actually run. Does not advance the
    251    * scheduler's clock;
    252    */
    253   public void idle() {
    254     idle(0, TimeUnit.MILLISECONDS);
    255   }
    256 
    257   /**
    258    * Causes {@link Runnable}s that have been scheduled to run within the next {@code intervalMillis} milliseconds to
    259    * run while advancing the scheduler's clock.
    260    *
    261    * @deprecated Use {@link #idle(long, TimeUnit)}.
    262    */
    263   @Deprecated
    264   public void idle(long intervalMillis) {
    265     idle(intervalMillis, TimeUnit.MILLISECONDS);
    266   }
    267 
    268   /**
    269    * Causes {@link Runnable}s that have been scheduled to run within the next specified amount of time to run while
    270    * advancing the scheduler's clock.
    271    */
    272   public void idle(long amount, TimeUnit unit) {
    273     getScheduler().advanceBy(amount, unit);
    274   }
    275 
    276   public void idleConstantly(boolean shouldIdleConstantly) {
    277     getScheduler().idleConstantly(shouldIdleConstantly);
    278   }
    279 
    280   /**
    281    * Causes all of the {@link Runnable}s that have been scheduled to run while advancing the scheduler's clock to the
    282    * start time of the last scheduled {@link Runnable}.
    283    */
    284   public void runToEndOfTasks() {
    285     getScheduler().advanceToLastPostedRunnable();
    286   }
    287 
    288   /**
    289    * Causes the next {@link Runnable}(s) that have been scheduled to run while advancing the scheduler's clock to its
    290    * start time. If more than one {@link Runnable} is scheduled to run at this time then they will all be run.
    291    */
    292   public void runToNextTask() {
    293     getScheduler().advanceToNextPostedRunnable();
    294   }
    295 
    296   /**
    297    * Causes only one of the next {@link Runnable}s that have been scheduled to run while advancing the scheduler's
    298    * clock to its start time. Only one {@link Runnable} will run even if more than one has ben scheduled to run at the
    299    * same time.
    300    */
    301   public void runOneTask() {
    302     getScheduler().runOneTask();
    303   }
    304 
    305   /**
    306    * Enqueue a task to be run later.
    307    *
    308    * @param runnable    the task to be run
    309    * @param delayMillis how many milliseconds into the (virtual) future to run it
    310    * @return true if the runnable is enqueued
    311    * @see android.os.Handler#postDelayed(Runnable,long)
    312    * @deprecated Use a {@link android.os.Handler} instance to post to a looper.
    313    */
    314   @Deprecated
    315   public boolean post(Runnable runnable, long delayMillis) {
    316     if (!quit) {
    317       getScheduler().postDelayed(runnable, delayMillis, TimeUnit.MILLISECONDS);
    318       return true;
    319     } else {
    320       return false;
    321     }
    322   }
    323 
    324   /**
    325    * Enqueue a task to be run ahead of all other delayed tasks.
    326    *
    327    * @param runnable    the task to be run
    328    * @return true if the runnable is enqueued
    329    * @see android.os.Handler#postAtFrontOfQueue(Runnable)
    330    * @deprecated Use a {@link android.os.Handler} instance to post to a looper.
    331    */
    332   @Deprecated
    333   public boolean postAtFrontOfQueue(Runnable runnable) {
    334     if (!quit) {
    335       getScheduler().postAtFrontOfQueue(runnable);
    336       return true;
    337     } else {
    338       return false;
    339     }
    340   }
    341 
    342   public void pause() {
    343     getScheduler().pause();
    344   }
    345 
    346   public void unPause() {
    347     getScheduler().unPause();
    348   }
    349 
    350   public boolean isPaused() {
    351     return getScheduler().isPaused();
    352   }
    353 
    354   public boolean setPaused(boolean shouldPause) {
    355     boolean wasPaused = isPaused();
    356     if (shouldPause) {
    357       pause();
    358     } else {
    359       unPause();
    360     }
    361     return wasPaused;
    362   }
    363 
    364   public void resetScheduler() {
    365     ShadowMessageQueue shadowMessageQueue = shadowOf(realObject.getQueue());
    366     if (realObject == Looper.getMainLooper() || RoboSettings.isUseGlobalScheduler()) {
    367       shadowMessageQueue.setScheduler(RuntimeEnvironment.getMasterScheduler());
    368     } else {
    369       shadowMessageQueue.setScheduler(new Scheduler());
    370     }
    371   }
    372 
    373   /**
    374    * Causes all enqueued tasks to be discarded, and pause state to be reset
    375    */
    376   public void reset() {
    377     shadowOf(realObject.getQueue()).reset();
    378     resetScheduler();
    379 
    380     quit = false;
    381   }
    382 
    383   /**
    384    * Returns the {@link org.robolectric.util.Scheduler} that is being used to manage the enqueued tasks.
    385    * This scheduler is managed by the Looper's associated queue.
    386    *
    387    * @return the {@link org.robolectric.util.Scheduler} that is being used to manage the enqueued tasks.
    388    */
    389   public Scheduler getScheduler() {
    390     return shadowOf(realObject.getQueue()).getScheduler();
    391   }
    392 
    393   public void runPaused(Runnable r) {
    394     boolean wasPaused = setPaused(true);
    395     try {
    396       r.run();
    397     } finally {
    398       if (!wasPaused) unPause();
    399     }
    400   }
    401 
    402   private static ShadowLooper shadowOf(Looper looper) {
    403     return Shadow.extract(looper);
    404   }
    405 
    406   private static ShadowMessageQueue shadowOf(MessageQueue mq) {
    407     return Shadow.extract(mq);
    408   }
    409 }
    410