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