1 package org.robolectric.shadows;
2 
3 import static android.os.Build.VERSION_CODES.N;
4 import static android.os.Build.VERSION_CODES.R;
5 import static android.os.Build.VERSION_CODES.S;
6 import static com.google.common.base.Preconditions.checkState;
7 import static org.robolectric.shadows.ShadowLooper.looperMode;
8 import static org.robolectric.util.reflector.Reflector.reflector;
9 
10 import android.os.Looper;
11 import android.view.Choreographer;
12 import android.view.Choreographer.FrameCallback;
13 import android.view.DisplayEventReceiver;
14 import java.time.Duration;
15 import org.robolectric.RuntimeEnvironment;
16 import org.robolectric.annotation.ClassName;
17 import org.robolectric.annotation.Implementation;
18 import org.robolectric.annotation.Implements;
19 import org.robolectric.annotation.LooperMode;
20 import org.robolectric.annotation.LooperMode.Mode;
21 import org.robolectric.annotation.RealObject;
22 import org.robolectric.annotation.Resetter;
23 import org.robolectric.util.PerfStatsCollector;
24 import org.robolectric.util.reflector.Accessor;
25 import org.robolectric.util.reflector.Direct;
26 import org.robolectric.util.reflector.ForType;
27 import org.robolectric.util.reflector.Static;
28 import org.robolectric.util.reflector.WithType;
29 
30 /**
31  * The shadow API for {@link android.view.Choreographer}.
32  *
33  * <p>Different shadow implementations will be used depending on the current {@link LooperMode}. See
34  * {@link ShadowLegacyChoreographer} and {@link ShadowPausedChoreographer} for details.
35  */
36 @Implements(value = Choreographer.class, shadowPicker = ShadowChoreographer.Picker.class)
37 public abstract class ShadowChoreographer {
38 
39   @RealObject Choreographer realObject;
40   private ChoreographerReflector reflector;
41 
42   private static volatile boolean isPaused = false;
43   private static volatile Duration frameDelay = Duration.ofMillis(1);
44 
45   /**
46    * This field is only used when {@link #isPaused()} is true. It represents the next scheduled
47    * vsync time (with respect to the system clock). See the {@link #getNextVsyncTime()} javadoc for
48    * more details.
49    */
50   private static volatile long nextVsyncTime;
51 
52   public static class Picker extends LooperShadowPicker<ShadowChoreographer> {
53 
Picker()54     public Picker() {
55       super(ShadowLegacyChoreographer.class, ShadowPausedChoreographer.class);
56     }
57   }
58 
59   /**
60    * Sets the delay between each frame. Note that the frames use the {@link ShadowSystemClock} and
61    * so have the same fidelity, when using the paused looper mode (which is the only mode supported
62    * by {@code ShadowDisplayEventReceiver}) the clock has millisecond fidelity.
63    *
64    * <p>Reasonable delays may be 15ms (approximating 60fps ~16.6ms), 10ms (approximating 90fps
65    * ~11.1ms), and 30ms (approximating 30fps ~33.3ms). Choosing too small of a frame delay may
66    * increase runtime as animation frames will have more steps.
67    *
68    * <p>Only works in {@link LooperMode.Mode#PAUSED} looper mode.
69    */
setFrameDelay(Duration delay)70   public static void setFrameDelay(Duration delay) {
71     checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY);
72     frameDelay = delay;
73   }
74 
75   /** See {@link #setFrameDelay(Duration)}. */
getFrameDelay()76   public static Duration getFrameDelay() {
77     checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY);
78     return frameDelay;
79   }
80 
81   /**
82    * Sets whether posting a frame should auto advance the clock or not. When paused the clock is not
83    * auto advanced, when unpaused the clock is advanced by the frame delay every time a frame
84    * callback is added. The default is not paused.
85    *
86    * <p>Only works in {@link LooperMode.Mode#PAUSED} looper mode.
87    */
setPaused(boolean paused)88   public static void setPaused(boolean paused) {
89     checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY);
90     isPaused = paused;
91   }
92 
93   /** See {@link #setPaused(boolean)}. */
isPaused()94   public static boolean isPaused() {
95     checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY);
96     return isPaused;
97   }
98 
99   /**
100    * This field is only used when {@link ShadowChoreographer#isPaused()} is true. It represents the
101    * next scheduled vsync time (with respect to the system clock). When the system clock is advanced
102    * to or beyond this time, a Choreographer frame will be triggered. It may be useful for tests to
103    * know when the next scheduled vsync time is in order to determine how long to idle the main
104    * looper in order to trigger the next Choreographer callback.
105    */
getNextVsyncTime()106   public static long getNextVsyncTime() {
107     return nextVsyncTime;
108   }
109 
setNextVsyncTime(long nextVsyncTime)110   static void setNextVsyncTime(long nextVsyncTime) {
111     ShadowChoreographer.nextVsyncTime = nextVsyncTime;
112   }
113 
114   /**
115    * Allows application to specify a fixed amount of delay when {@link #postCallback(int, Runnable,
116    * Object)} is invoked. The default delay value is 0. This can be used to avoid infinite animation
117    * tasks to be spawned when the Robolectric {@link org.robolectric.util.Scheduler} is in {@link
118    * org.robolectric.util.Scheduler.IdleState#PAUSED} mode.
119    *
120    * <p>Only supported in {@link LooperMode.Mode#LEGACY}
121    *
122    * @deprecated Use the {@link Mode#PAUSED} looper instead.
123    */
124   @Deprecated
setPostCallbackDelay(int delayMillis)125   public static void setPostCallbackDelay(int delayMillis) {
126     checkState(ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper must be %s", Mode.LEGACY);
127     ShadowLegacyChoreographer.setPostCallbackDelay(delayMillis);
128   }
129 
130   /**
131    * Allows application to specify a fixed amount of delay when {@link
132    * #postFrameCallback(FrameCallback)} is invoked. The default delay value is 0. This can be used
133    * to avoid infinite animation tasks to be spawned when in LooperMode PAUSED or {@link
134    * org.robolectric.util.Scheduler.IdleState#PAUSED} and displaying an animation.
135    *
136    * @deprecated Use the {@link Mode#PAUSED} looper and {@link #setPaused(boolean)} and {@link
137    *     #setFrameDelay(Duration)} to configure the vsync event behavior.
138    */
139   @Deprecated
setPostFrameCallbackDelay(int delayMillis)140   public static void setPostFrameCallbackDelay(int delayMillis) {
141     if (looperMode() == Mode.LEGACY) {
142       ShadowLegacyChoreographer.setPostFrameCallbackDelay(delayMillis);
143     } else {
144       setPaused(delayMillis != 0);
145       setFrameDelay(Duration.ofMillis(delayMillis == 0 ? 1 : delayMillis));
146     }
147   }
148 
149   /**
150    * Return the current inter-frame interval.
151    *
152    * <p>Can only be used in {@link LooperMode.Mode#LEGACY}
153    *
154    * @return Inter-frame interval.
155    * @deprecated Use the {@link Mode#PAUSED} looper and {@link #getFrameDelay()} to configure the
156    *     frame delay.
157    */
158   @Deprecated
getFrameInterval()159   public static long getFrameInterval() {
160     checkState(ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper must be %s", Mode.LEGACY);
161     return ShadowLegacyChoreographer.getFrameInterval();
162   }
163 
164   /**
165    * Set the inter-frame interval used to advance the clock. By default, this is set to 1ms.
166    *
167    * <p>Only supported in {@link LooperMode.Mode#LEGACY}
168    *
169    * @param frameInterval Inter-frame interval.
170    * @deprecated Use the {@link Mode#PAUSED} looper and {@link #setFrameDelay(Duration)} to
171    *     configure the frame delay.
172    */
173   @Deprecated
setFrameInterval(long frameInterval)174   public static void setFrameInterval(long frameInterval) {
175     checkState(ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper must be %s", Mode.LEGACY);
176     ShadowLegacyChoreographer.setFrameInterval(frameInterval);
177   }
178 
179   @Implementation(maxSdk = R)
doFrame(long frameTimeNanos, int frame)180   protected void doFrame(long frameTimeNanos, int frame) {
181     if (reflector == null) {
182       reflector = reflector(ChoreographerReflector.class, realObject);
183     }
184     PerfStatsCollector.getInstance()
185         .measure("doFrame", () -> reflector.doFrame(frameTimeNanos, frame));
186   }
187 
188   @Implementation(minSdk = S)
doFrame( long frameTimeNanos, int frame, @ClassName("android.view.DisplayEventReceiver$VsyncEventData") Object vsyncEventData)189   protected void doFrame(
190       long frameTimeNanos,
191       int frame,
192       @ClassName("android.view.DisplayEventReceiver$VsyncEventData") Object vsyncEventData) {
193     if (reflector == null) {
194       reflector = reflector(ChoreographerReflector.class, realObject);
195     }
196     PerfStatsCollector.getInstance()
197         .measure("doFrame", () -> reflector.doFrame(frameTimeNanos, frame, vsyncEventData));
198   }
199 
200   @Resetter
reset()201   public static void reset() {
202     nextVsyncTime = 0;
203     isPaused = false;
204     frameDelay = Duration.ofMillis(1);
205     if (RuntimeEnvironment.getApiLevel() >= N) {
206       ShadowBackdropFrameRenderer.reset();
207     }
208   }
209 
210   /** Accessor interface for {@link Choreographer}'s internals */
211   @ForType(Choreographer.class)
212   protected interface ChoreographerReflector {
213 
214     @Accessor("mLastFrameTimeNanos")
setLastFrameTimeNanos(long time)215     void setLastFrameTimeNanos(long time);
216 
217     @Accessor("mCallbackQueues")
getCallbackQueues()218     Object[] getCallbackQueues();
219 
220     @Accessor("mCallbackPool")
setCallbackPool(Object callbackPool)221     void setCallbackPool(Object callbackPool);
222 
223     @Accessor("mFrameScheduled")
setFrameScheduled(boolean frameScheduled)224     void setFrameScheduled(boolean frameScheduled);
225 
226     @Accessor("mCallbacksRunning")
setCallbacksRunning(boolean callbacksRunning)227     void setCallbacksRunning(boolean callbacksRunning);
228 
229     @Direct
doFrame(long frameTimeNanos, int frame)230     void doFrame(long frameTimeNanos, int frame);
231 
232     @Direct
doFrame( long frameTimeNanos, int frame, @WithType("android.view.DisplayEventReceiver$VsyncEventData") Object vsyncEventData)233     void doFrame(
234         long frameTimeNanos,
235         int frame,
236         @WithType("android.view.DisplayEventReceiver$VsyncEventData") Object vsyncEventData);
237 
238     @Accessor("mDisplayEventReceiver")
getReceiver()239     DisplayEventReceiver getReceiver();
240 
241     @Accessor("sThreadInstance")
242     @Static
getThreadInstance()243     ThreadLocal<Choreographer> getThreadInstance();
244 
245     @Accessor("mLooper")
getLooper()246     Looper getLooper();
247 
248     @Direct
__constructor__(Looper looper)249     void __constructor__(Looper looper);
250 
251     @Direct
__constructor__(Looper looper, int vsyncSource, long layerHandle)252     void __constructor__(Looper looper, int vsyncSource, long layerHandle);
253 
254     @Direct
__constructor__(Looper looper, int vsyncSource)255     void __constructor__(Looper looper, int vsyncSource);
256 
257     @Accessor("mFrameData")
getFrameData()258     /*android.view.Choreographer$FrameData*/ Object getFrameData();
259 
260     @Accessor("mLastFrameIntervalNanos")
setLastFrameIntervalNanos(long val)261     void setLastFrameIntervalNanos(long val);
262 
263     @Accessor("mFrameInfo")
getFrameInfo()264     Object /* FrameInfo */ getFrameInfo();
265   }
266 
267   /** Accessor interface for {@link Choreographer}'s CallbackQueue internals */
268   @ForType(className = "android.view.Choreographer$CallbackQueue")
269   protected interface CallbackQueueReflector {
270     @Accessor("mHead")
setHead(Object head)271     void setHead(Object head);
272   }
273 }
274