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