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 java.time.Duration; 9 import org.robolectric.annotation.Implementation; 10 import org.robolectric.annotation.Implements; 11 import org.robolectric.annotation.LooperMode; 12 import org.robolectric.annotation.Resetter; 13 import org.robolectric.shadow.api.Shadow; 14 import org.robolectric.util.SoftThreadLocal; 15 16 /** 17 * The {@link Choreographer} shadow for {@link LooperMode.Mode.PAUSED}. 18 * 19 * <p>In {@link LooperMode.Mode.PAUSED} mode, Robolectric maintains its own concept of the current 20 * time from the Choreographer's point of view, aimed at making animations work correctly. Time 21 * starts out at 0 and advances by {@code frameInterval} every time {@link 22 * Choreographer#getFrameTimeNanos()} is called. 23 */ 24 @Implements( 25 value = Choreographer.class, 26 shadowPicker = ShadowChoreographer.Picker.class, 27 isInAndroidSdk = false) 28 public class ShadowLegacyChoreographer extends ShadowChoreographer { 29 private long nanoTime = 0; 30 private static long FRAME_INTERVAL = Duration.ofMillis(10).toNanos(); 31 private static final Thread MAIN_THREAD = Thread.currentThread(); 32 private static SoftThreadLocal<Choreographer> instance = makeThreadLocal(); 33 private Handler handler = new Handler(Looper.myLooper()); 34 private static volatile int postCallbackDelayMillis = 0; 35 private static volatile int postFrameCallbackDelayMillis = 0; 36 37 @SuppressWarnings("ReturnValueIgnored") makeThreadLocal()38 private static SoftThreadLocal<Choreographer> makeThreadLocal() { 39 return new SoftThreadLocal<Choreographer>() { 40 @Override 41 protected Choreographer create() { 42 Looper looper = Looper.myLooper(); 43 if (looper == null) { 44 throw new IllegalStateException("The current thread must have a looper!"); 45 } 46 47 // Choreographer's constructor changes somewhere in Android O... 48 try { 49 Choreographer.class.getDeclaredConstructor(Looper.class); 50 return Shadow.newInstance( 51 Choreographer.class, new Class[] {Looper.class}, new Object[] {looper}); 52 } catch (NoSuchMethodException e) { 53 return Shadow.newInstance( 54 Choreographer.class, new Class[] {Looper.class, int.class}, new Object[] {looper, 0}); 55 } 56 } 57 }; 58 } 59 60 /** 61 * Allows application to specify a fixed amount of delay when {@link #postCallback(int, Runnable, 62 * Object)} is invoked. The default delay value is 0. This can be used to avoid infinite animation 63 * tasks to be spawned when the Robolectric {@link org.robolectric.util.Scheduler} is in {@link 64 * org.robolectric.util.Scheduler.IdleState#PAUSED} mode. 65 */ 66 public static void setPostCallbackDelay(int delayMillis) { 67 postCallbackDelayMillis = delayMillis; 68 } 69 70 /** 71 * Allows application to specify a fixed amount of delay when {@link 72 * #postFrameCallback(FrameCallback)} is invoked. The default delay value is 0. This can be used 73 * to avoid infinite animation tasks to be spawned when the Robolectric {@link 74 * org.robolectric.util.Scheduler} is in {@link org.robolectric.util.Scheduler.IdleState#PAUSED} 75 * mode. 76 */ 77 public static void setPostFrameCallbackDelay(int delayMillis) { 78 postFrameCallbackDelayMillis = delayMillis; 79 } 80 81 @Implementation 82 protected static Choreographer getInstance() { 83 return instance.get(); 84 } 85 86 /** 87 * The default implementation will call {@link #postCallbackDelayed(int, Runnable, Object, long)} 88 * with no delay. {@link android.animation.AnimationHandler} calls this method to schedule 89 * animation updates infinitely. Because during a Robolectric test the system time is paused and 90 * execution of the event loop is invoked for each test instruction, the behavior of 91 * AnimationHandler would result in endless looping (the execution of the task results in a new 92 * animation task created and scheduled to the front of the event loop queue). 93 * 94 * <p>To prevent endless looping, a test may call {@link #setPostCallbackDelay(int)} to specify a 95 * small delay when animation is scheduled. 96 * 97 * @see #setPostCallbackDelay(int) 98 */ 99 @Implementation 100 protected void postCallback(int callbackType, Runnable action, Object token) { 101 postCallbackDelayed(callbackType, action, token, postCallbackDelayMillis); 102 } 103 104 @Implementation 105 protected void postCallbackDelayed( 106 int callbackType, Runnable action, Object token, long delayMillis) { 107 handler.postDelayed(action, delayMillis); 108 } 109 110 @Implementation 111 protected void removeCallbacks(int callbackType, Runnable action, Object token) { 112 handler.removeCallbacks(action, token); 113 } 114 115 /** 116 * The default implementation will call {@link #postFrameCallbackDelayed(FrameCallback, long)} 117 * with no delay. {@link android.animation.AnimationHandler} calls this method to schedule 118 * animation updates infinitely. Because during a Robolectric test the system time is paused and 119 * execution of the event loop is invoked for each test instruction, the behavior of 120 * AnimationHandler would result in endless looping (the execution of the task results in a new 121 * animation task created and scheduled to the front of the event loop queue). 122 * 123 * <p>To prevent endless looping, a test may call {@link #setPostFrameCallbackDelay(int)} to 124 * specify a small delay when animation is scheduled. 125 * 126 * @see #setPostCallbackDelay(int) 127 */ 128 @Implementation 129 protected void postFrameCallback(final FrameCallback callback) { 130 postFrameCallbackDelayed(callback, postFrameCallbackDelayMillis); 131 } 132 133 @Implementation 134 protected void postFrameCallbackDelayed(final FrameCallback callback, long delayMillis) { 135 handler.postAtTime( 136 new Runnable() { 137 @Override 138 public void run() { 139 callback.doFrame(getFrameTimeNanos()); 140 } 141 }, 142 callback, 143 SystemClock.uptimeMillis() + delayMillis); 144 } 145 146 @Implementation 147 protected void removeFrameCallback(FrameCallback callback) { 148 handler.removeCallbacksAndMessages(callback); 149 } 150 151 @Implementation 152 protected long getFrameTimeNanos() { 153 final long now = nanoTime; 154 nanoTime += ShadowLegacyChoreographer.FRAME_INTERVAL; 155 return now; 156 } 157 158 /** 159 * Return the current inter-frame interval. 160 * 161 * @return Inter-frame interval. 162 */ 163 public static long getFrameInterval() { 164 return ShadowLegacyChoreographer.FRAME_INTERVAL; 165 } 166 167 /** 168 * Set the inter-frame interval used to advance the clock. By default, this is set to 1ms. 169 * 170 * @param frameInterval Inter-frame interval. 171 */ 172 public static void setFrameInterval(long frameInterval) { 173 ShadowLegacyChoreographer.FRAME_INTERVAL = frameInterval; 174 } 175 176 @Resetter 177 public static synchronized void reset() { 178 // Blech. We need to share the main looper because somebody might refer to it in a static 179 // field. We also need to keep it in a soft reference so we don't max out permgen. 180 if (Thread.currentThread() != MAIN_THREAD) { 181 throw new RuntimeException("You should only call this from the main thread!"); 182 } 183 instance = makeThreadLocal(); 184 FRAME_INTERVAL = Duration.ofMillis(10).toNanos(); 185 postCallbackDelayMillis = 0; 186 postFrameCallbackDelayMillis = 0; 187 } 188 } 189