Home | History | Annotate | Download | only in shadows
      1 package org.robolectric.shadows;
      2 
      3 import android.os.Handler;
      4 import android.os.Looper;
      5 import android.os.SystemClock;
      6 import android.view.Choreographer;
      7 import android.view.Choreographer.FrameCallback;
      8 import org.robolectric.annotation.Implementation;
      9 import org.robolectric.annotation.Implements;
     10 import org.robolectric.annotation.Resetter;
     11 import org.robolectric.shadow.api.Shadow;
     12 import org.robolectric.util.SoftThreadLocal;
     13 import org.robolectric.util.TimeUtils;
     14 
     15 /**
     16  * Robolectric maintains its own concept of the current time from the Choreographer's
     17  * point of view, aimed at making animations work correctly. Time starts out at {@code 0}
     18  * and advances by {@code frameInterval} every time
     19  * {@link Choreographer#getFrameTimeNanos()} is called.
     20  */
     21 @Implements(Choreographer.class)
     22 public class ShadowChoreographer {
     23   private long nanoTime = 0;
     24   private static long FRAME_INTERVAL = 10 * TimeUtils.NANOS_PER_MS; // 10ms
     25   private static final Thread MAIN_THREAD = Thread.currentThread();
     26   private static SoftThreadLocal<Choreographer> instance = makeThreadLocal();
     27   private Handler handler = new Handler(Looper.myLooper());
     28   private static volatile int postCallbackDelayMillis = 0;
     29   private static volatile int postFrameCallbackDelayMillis = 0;
     30 
     31   private static SoftThreadLocal<Choreographer> makeThreadLocal() {
     32     return new SoftThreadLocal<Choreographer>() {
     33       @Override
     34       protected Choreographer create() {
     35         Looper looper = Looper.myLooper();
     36         if (looper == null) {
     37           throw new IllegalStateException("The current thread must have a looper!");
     38         }
     39 
     40         // Choreographer's constructor changes somewhere in Android O...
     41         try {
     42           Choreographer.class.getDeclaredConstructor(Looper.class);
     43           return Shadow.newInstance(Choreographer.class, new Class[]{Looper.class}, new Object[]{looper});
     44         } catch (NoSuchMethodException e) {
     45           return Shadow.newInstance(Choreographer.class, new Class[]{Looper.class, int.class}, new Object[]{looper, 0});
     46         }
     47       }
     48     };
     49   }
     50 
     51   /**
     52    * Allows application to specify a fixed amount of delay when {@link #postCallback(int, Runnable,
     53    * Object)} is invoked. The default delay value is `0`. This can be used to avoid infinite
     54    * animation tasks to be spawned when the Robolectric {@link org.robolectric.util.Scheduler} is in
     55    * {@link org.robolectric.util.Scheduler.IdleState#PAUSED} mode.
     56    */
     57   public static void setPostCallbackDelay(int delayMillis) {
     58     postCallbackDelayMillis = delayMillis;
     59   }
     60 
     61   /**
     62    * Allows application to specify a fixed amount of delay when {@link
     63    * #postFrameCallback(FrameCallback)} is invoked. The default delay value is `0`. This can be used
     64    * to avoid infinite animation tasks to be spawned when the Robolectric {@link
     65    * org.robolectric.util.Scheduler} is in {@link org.robolectric.util.Scheduler.IdleState#PAUSED}
     66    * mode.
     67    */
     68   public static void setPostFrameCallbackDelay(int delayMillis) {
     69     postFrameCallbackDelayMillis = delayMillis;
     70   }
     71 
     72   @Implementation
     73   public static Choreographer getInstance() {
     74     return instance.get();
     75   }
     76 
     77   /**
     78    * The default implementation will call {@link #postCallbackDelayed(int, Runnable, Object, long)}
     79    * with no delay. {@link android.animation.AnimationHandler} calls this method to schedule
     80    * animation updates infinitely. Because during a Robolectric test the system time is paused and
     81    * execution of the event loop is invoked for each test instruction, the behavior of
     82    * AnimationHandler would result in endless looping (the execution of the task results in a new
     83    * animation task created and scheduled to the front of the event loop queue).
     84    *
     85    * To prevent endless looping, a test may call {@link #setPostCallbackDelay(int)} to specify a
     86    * small delay when animation is scheduled.
     87    *
     88    * @see #setPostCallbackDelay(int)
     89    */
     90   @Implementation
     91   public void postCallback(int callbackType, Runnable action, Object token) {
     92     postCallbackDelayed(callbackType, action, token, postCallbackDelayMillis);
     93   }
     94 
     95   @Implementation
     96   public void postCallbackDelayed(int callbackType, Runnable action, Object token, long delayMillis) {
     97     handler.postDelayed(action, delayMillis);
     98   }
     99 
    100   @Implementation
    101   public void removeCallbacks(int callbackType, Runnable action, Object token) {
    102     handler.removeCallbacks(action, token);
    103   }
    104 
    105   /**
    106    * The default implementation will call {@link #postFrameCallbackDelayed(FrameCallback, long)}
    107    * with no delay. {@link android.animation.AnimationHandler} calls this method to schedule
    108    * animation updates infinitely. Because during a Robolectric test the system time is paused and
    109    * execution of the event loop is invoked for each test instruction, the behavior of
    110    * AnimationHandler would result in endless looping (the execution of the task results in a new
    111    * animation task created and scheduled to the front of the event loop queue).
    112    *
    113    * To prevent endless looping, a test may call {@link #setPostFrameCallbackDelay(int)} to
    114    * specify a small delay when animation is scheduled.
    115    *
    116    * @see #setPostCallbackDelay(int)
    117    */
    118   @Implementation
    119   public void postFrameCallback(final FrameCallback callback) {
    120     postFrameCallbackDelayed(callback, postFrameCallbackDelayMillis);
    121   }
    122 
    123   @Implementation
    124   public void postFrameCallbackDelayed(final FrameCallback callback, long delayMillis) {
    125     handler.postAtTime(new Runnable() {
    126       @Override public void run() {
    127         callback.doFrame(getFrameTimeNanos());
    128       }
    129     }, callback, SystemClock.uptimeMillis() + delayMillis);
    130   }
    131 
    132   @Implementation
    133   public void removeFrameCallback(FrameCallback callback) {
    134     handler.removeCallbacksAndMessages(callback);
    135   }
    136 
    137   @Implementation
    138   public long getFrameTimeNanos() {
    139     final long now = nanoTime;
    140     nanoTime += ShadowChoreographer.FRAME_INTERVAL;
    141     return now;
    142   }
    143 
    144   /**
    145    * Return the current inter-frame interval.
    146    *
    147    * @return  Inter-frame interval.
    148    */
    149   public static long getFrameInterval() {
    150     return ShadowChoreographer.FRAME_INTERVAL;
    151   }
    152 
    153   /**
    154    * Set the inter-frame interval used to advance the clock. By default, this is set to 1ms.
    155    *
    156    * @param frameInterval  Inter-frame interval.
    157    */
    158   public static void setFrameInterval(long frameInterval) {
    159     ShadowChoreographer.FRAME_INTERVAL = frameInterval;
    160   }
    161 
    162   @Resetter
    163   public static synchronized void reset() {
    164     // Blech. We need to share the main looper because somebody might refer to it in a static
    165     // field. We also need to keep it in a soft reference so we don't max out permgen.
    166     if (Thread.currentThread() != MAIN_THREAD) {
    167       throw new RuntimeException("You should only call this from the main thread!");
    168     }
    169     instance = makeThreadLocal();
    170     FRAME_INTERVAL = 10 * TimeUtils.NANOS_PER_MS; // 10ms
    171   }
    172 }
    173 
    174