• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.robolectric.shadows;
2 
3 import static android.os.Build.VERSION_CODES.R;
4 import static com.google.common.base.Preconditions.checkState;
5 import static org.robolectric.shadows.ShadowLooper.looperMode;
6 import static org.robolectric.util.reflector.Reflector.reflector;
7 
8 import android.view.Choreographer;
9 import android.view.Choreographer.FrameCallback;
10 import android.view.DisplayEventReceiver;
11 import java.time.Duration;
12 import org.robolectric.annotation.Implementation;
13 import org.robolectric.annotation.Implements;
14 import org.robolectric.annotation.LooperMode;
15 import org.robolectric.annotation.LooperMode.Mode;
16 import org.robolectric.annotation.RealObject;
17 import org.robolectric.annotation.Resetter;
18 import org.robolectric.util.PerfStatsCollector;
19 import org.robolectric.util.reflector.Accessor;
20 import org.robolectric.util.reflector.Direct;
21 import org.robolectric.util.reflector.ForType;
22 import org.robolectric.util.reflector.Static;
23 
24 /**
25  * The shadow API for {@link android.view.Choreographer}.
26  *
27  * <p>Different shadow implementations will be used depending on the current {@link LooperMode}. See
28  * {@link ShadowLegacyChoreographer} and {@link ShadowPausedChoreographer} for details.
29  */
30 @Implements(value = Choreographer.class, shadowPicker = ShadowChoreographer.Picker.class)
31 public abstract class ShadowChoreographer {
32 
33   @RealObject Choreographer realObject;
34   private ChoreographerReflector reflector;
35 
36   private static volatile boolean isPaused = false;
37   private static volatile Duration frameDelay = Duration.ofMillis(1);
38 
39   public static class Picker extends LooperShadowPicker<ShadowChoreographer> {
40 
Picker()41     public Picker() {
42       super(ShadowLegacyChoreographer.class, ShadowPausedChoreographer.class);
43     }
44   }
45 
46   /**
47    * Sets the delay between each frame. Note that the frames use the {@link ShadowSystemClock} and
48    * so have the same fidelity, when using the paused looper mode (which is the only mode supported
49    * by {@code ShadowDisplayEventReceiver}) the clock has millisecond fidelity.
50    *
51    * <p>Reasonable delays may be 15ms (approximating 60fps ~16.6ms), 10ms (approximating 90fps
52    * ~11.1ms), and 30ms (approximating 30fps ~33.3ms). Choosing too small of a frame delay may
53    * increase runtime as animation frames will have more steps.
54    *
55    * <p>Only works in {@link LooperMode.Mode#PAUSED} looper mode.
56    */
setFrameDelay(Duration delay)57   public static void setFrameDelay(Duration delay) {
58     checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY);
59     frameDelay = delay;
60   }
61 
62   /** See {@link #setFrameDelay(Duration)}. */
getFrameDelay()63   public static Duration getFrameDelay() {
64     checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY);
65     return frameDelay;
66   }
67 
68   /**
69    * Sets whether posting a frame should auto advance the clock or not. When paused the clock is not
70    * auto advanced, when unpaused the clock is advanced by the frame delay every time a frame
71    * callback is added. The default is not paused.
72    *
73    * <p>Only works in {@link LooperMode.Mode#PAUSED} looper mode.
74    */
setPaused(boolean paused)75   public static void setPaused(boolean paused) {
76     checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY);
77     isPaused = paused;
78   }
79 
80   /** See {@link #setPaused(boolean)}. */
isPaused()81   public static boolean isPaused() {
82     checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY);
83     return isPaused;
84   }
85 
86   /**
87    * Allows application to specify a fixed amount of delay when {@link #postCallback(int, Runnable,
88    * Object)} is invoked. The default delay value is 0. This can be used to avoid infinite animation
89    * tasks to be spawned when the Robolectric {@link org.robolectric.util.Scheduler} is in {@link
90    * org.robolectric.util.Scheduler.IdleState#PAUSED} mode.
91    *
92    * <p>Only supported in {@link LooperMode.Mode#LEGACY}
93    *
94    * @deprecated Use the {@link Mode#PAUSED} looper instead.
95    */
96   @Deprecated
setPostCallbackDelay(int delayMillis)97   public static void setPostCallbackDelay(int delayMillis) {
98     checkState(ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper must be %s", Mode.LEGACY);
99     ShadowLegacyChoreographer.setPostCallbackDelay(delayMillis);
100   }
101 
102   /**
103    * Allows application to specify a fixed amount of delay when {@link
104    * #postFrameCallback(FrameCallback)} is invoked. The default delay value is 0. This can be used
105    * to avoid infinite animation tasks to be spawned when in LooperMode PAUSED or {@link
106    * org.robolectric.util.Scheduler.IdleState#PAUSED} and displaying an animation.
107    *
108    * @deprecated Use the {@link Mode#PAUSED} looper and {@link #setPaused(boolean)} and {@link
109    *     #setFrameDelay(Duration)} to configure the vsync event behavior.
110    */
111   @Deprecated
setPostFrameCallbackDelay(int delayMillis)112   public static void setPostFrameCallbackDelay(int delayMillis) {
113     if (looperMode() == Mode.LEGACY) {
114       ShadowLegacyChoreographer.setPostFrameCallbackDelay(delayMillis);
115     } else {
116       setPaused(delayMillis != 0);
117       setFrameDelay(Duration.ofMillis(delayMillis == 0 ? 1 : delayMillis));
118     }
119   }
120 
121   /**
122    * Return the current inter-frame interval.
123    *
124    * <p>Can only be used in {@link LooperMode.Mode#LEGACY}
125    *
126    * @return Inter-frame interval.
127    * @deprecated Use the {@link Mode#PAUSED} looper and {@link #getFrameDelay()} to configure the
128    *     frame delay.
129    */
130   @Deprecated
getFrameInterval()131   public static long getFrameInterval() {
132     checkState(ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper must be %s", Mode.LEGACY);
133     return ShadowLegacyChoreographer.getFrameInterval();
134   }
135 
136   /**
137    * Set the inter-frame interval used to advance the clock. By default, this is set to 1ms.
138    *
139    * <p>Only supported in {@link LooperMode.Mode#LEGACY}
140    *
141    * @param frameInterval Inter-frame interval.
142    * @deprecated Use the {@link Mode#PAUSED} looper and {@link #setFrameDelay(Duration)} to
143    *     configure the frame delay.
144    */
145   @Deprecated
setFrameInterval(long frameInterval)146   public static void setFrameInterval(long frameInterval) {
147     checkState(ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper must be %s", Mode.LEGACY);
148     ShadowLegacyChoreographer.setFrameInterval(frameInterval);
149   }
150 
151   @Implementation(maxSdk = R)
doFrame(long frameTimeNanos, int frame)152   protected void doFrame(long frameTimeNanos, int frame) {
153     if (reflector == null) {
154       reflector = reflector(ChoreographerReflector.class, realObject);
155     }
156     PerfStatsCollector.getInstance()
157         .measure("doFrame", () -> reflector.doFrame(frameTimeNanos, frame));
158   }
159 
160   @Resetter
reset()161   public static void reset() {
162     isPaused = false;
163     frameDelay = Duration.ofMillis(1);
164   }
165 
166   /** Accessor interface for {@link Choreographer}'s internals */
167   @ForType(Choreographer.class)
168   protected interface ChoreographerReflector {
169     @Accessor("sThreadInstance")
170     @Static
getThreadInstance()171     ThreadLocal<Choreographer> getThreadInstance();
172 
173     @Direct
doFrame(long frameTimeNanos, int frame)174     void doFrame(long frameTimeNanos, int frame);
175 
176     @Accessor("mDisplayEventReceiver")
getReceiver()177     DisplayEventReceiver getReceiver();
178   }
179 }
180