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