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 
makeThreadLocal()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   protected 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    * <p>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   protected void postCallback(int callbackType, Runnable action, Object token) {
92     postCallbackDelayed(callbackType, action, token, postCallbackDelayMillis);
93   }
94 
95   @Implementation
96   protected void postCallbackDelayed(
97       int callbackType, Runnable action, Object token, long delayMillis) {
98     handler.postDelayed(action, delayMillis);
99   }
100 
101   @Implementation
102   protected void removeCallbacks(int callbackType, Runnable action, Object token) {
103     handler.removeCallbacks(action, token);
104   }
105 
106   /**
107    * The default implementation will call {@link #postFrameCallbackDelayed(FrameCallback, long)}
108    * with no delay. {@link android.animation.AnimationHandler} calls this method to schedule
109    * animation updates infinitely. Because during a Robolectric test the system time is paused and
110    * execution of the event loop is invoked for each test instruction, the behavior of
111    * AnimationHandler would result in endless looping (the execution of the task results in a new
112    * animation task created and scheduled to the front of the event loop queue).
113    *
114    * <p>To prevent endless looping, a test may call {@link #setPostFrameCallbackDelay(int)} to
115    * specify a small delay when animation is scheduled.
116    *
117    * @see #setPostCallbackDelay(int)
118    */
119   @Implementation
120   protected void postFrameCallback(final FrameCallback callback) {
121     postFrameCallbackDelayed(callback, postFrameCallbackDelayMillis);
122   }
123 
124   @Implementation
125   protected void postFrameCallbackDelayed(final FrameCallback callback, long delayMillis) {
126     handler.postAtTime(new Runnable() {
127       @Override public void run() {
128         callback.doFrame(getFrameTimeNanos());
129       }
130     }, callback, SystemClock.uptimeMillis() + delayMillis);
131   }
132 
133   @Implementation
134   protected void removeFrameCallback(FrameCallback callback) {
135     handler.removeCallbacksAndMessages(callback);
136   }
137 
138   @Implementation
139   protected long getFrameTimeNanos() {
140     final long now = nanoTime;
141     nanoTime += ShadowChoreographer.FRAME_INTERVAL;
142     return now;
143   }
144 
145   /**
146    * Return the current inter-frame interval.
147    *
148    * @return  Inter-frame interval.
149    */
150   public static long getFrameInterval() {
151     return ShadowChoreographer.FRAME_INTERVAL;
152   }
153 
154   /**
155    * Set the inter-frame interval used to advance the clock. By default, this is set to 1ms.
156    *
157    * @param frameInterval  Inter-frame interval.
158    */
159   public static void setFrameInterval(long frameInterval) {
160     ShadowChoreographer.FRAME_INTERVAL = frameInterval;
161   }
162 
163   @Resetter
164   public static synchronized void reset() {
165     // Blech. We need to share the main looper because somebody might refer to it in a static
166     // field. We also need to keep it in a soft reference so we don't max out permgen.
167     if (Thread.currentThread() != MAIN_THREAD) {
168       throw new RuntimeException("You should only call this from the main thread!");
169     }
170     instance = makeThreadLocal();
171     FRAME_INTERVAL = 10 * TimeUtils.NANOS_PER_MS; // 10ms
172   }
173 }
174 
175