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