All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.robolectric.shadows.ShadowChoreographer Maven / Gradle / Ivy

The newest version!
package org.robolectric.shadows;

import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.view.Choreographer;
import android.view.Choreographer.FrameCallback;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.Resetter;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.SoftThreadLocal;
import org.robolectric.util.TimeUtils;

/**
 * Robolectric maintains its own concept of the current time from the Choreographer's
 * point of view, aimed at making animations work correctly. Time starts out at {@code 0}
 * and advances by {@code frameInterval} every time
 * {@link Choreographer#getFrameTimeNanos()} is called.
 */
@Implements(Choreographer.class)
public class ShadowChoreographer {
  private long nanoTime = 0;
  private static long FRAME_INTERVAL = 10 * TimeUtils.NANOS_PER_MS; // 10ms
  private static final Thread MAIN_THREAD = Thread.currentThread();
  private static SoftThreadLocal instance = makeThreadLocal();
  private Handler handler = new Handler(Looper.myLooper());
  private static volatile int postCallbackDelayMillis = 0;

  private static SoftThreadLocal makeThreadLocal() {
    return new SoftThreadLocal() {
      @Override
      protected Choreographer create() {
        Looper looper = Looper.myLooper();
        if (looper == null) {
          throw new IllegalStateException("The current thread must have a looper!");
        }

        // Choreographer's constructor changes somewhere in Android O...
        try {
          Choreographer.class.getDeclaredConstructor(Looper.class);
          return Shadow.newInstance(Choreographer.class, new Class[]{Looper.class}, new Object[]{looper});
        } catch (NoSuchMethodException e) {
          return Shadow.newInstance(Choreographer.class, new Class[]{Looper.class, int.class}, new Object[]{looper, 0});
        }
      }
    };
  }

  /**
   * Allows application to specify a fixed amount of delay when
   * {@link #postFrameCallback(FrameCallback)} or {@link #postCallback(int, Runnable, Object)} is
   * invoked. The default delay value is `0`. This can be used to avoid infinite animation tasks to
   * be spawned when the Robolectric {@link org.robolectric.util.Scheduler} is in
   * {@link org.robolectric.util.Scheduler.IdleState#PAUSED} mode.
   */
  public static void setPostCallbackDelay(int delayMillis) {
    postCallbackDelayMillis = delayMillis;
  }

  @Implementation
  public static Choreographer getInstance() {
    return instance.get();
  }

  /**
   * The default implementation will call {@link #postCallbackDelayed(int, Runnable, Object, long)}
   * with no delay. {@link android.animation.AnimationHandler} calls this method to schedule
   * animation updates infinitely. Because during a Robolectric test the system time is paused and
   * execution of the event loop is invoked for each test instruction, the behavior of
   * AnimationHandler would result in endless looping (the execution of the task results in a new
   * animation task created and scheduled to the front of the event loop queue).
   *
   * To prevent endless looping, a test may call {@link #setPostCallbackDelay(int)} to specify a
   * small delay when animation is scheduled.
   *
   * @see #setPostCallbackDelay(int)
   */
  @Implementation
  public void postCallback(int callbackType, Runnable action, Object token) {
    postCallbackDelayed(callbackType, action, token, postCallbackDelayMillis);
  }

  @Implementation
  public void postCallbackDelayed(int callbackType, Runnable action, Object token, long delayMillis) {
    handler.postDelayed(action, delayMillis);
  }

  @Implementation
  public void removeCallbacks(int callbackType, Runnable action, Object token) {
    handler.removeCallbacks(action, token);
  }

  /**
   * The default implementation will call {@link #postFrameCallbackDelayed(FrameCallback, long)}
   * with no delay. {@link android.animation.AnimationHandler} calls this method to schedule
   * animation updates infinitely. Because during a Robolectric test the system time is paused and
   * execution of the event loop is invoked for each test instruction, the behavior of
   * AnimationHandler would result in endless looping (the execution of the task results in a new
   * animation task created and scheduled to the front of the event loop queue).
   *
   * To prevent endless looping, a test may call {@link #setPostCallbackDelay(int)} to specify a
   * small delay when animation is scheduled.
   *
   * @see #setPostCallbackDelay(int)
   */
  @Implementation
  public void postFrameCallback(final Choreographer.FrameCallback callback) {
    postFrameCallbackDelayed(callback, postCallbackDelayMillis);
  }

  @Implementation
  public void postFrameCallbackDelayed(final Choreographer.FrameCallback callback, long delayMillis) {
    handler.postAtTime(new Runnable() {
      @Override public void run() {
        callback.doFrame(getFrameTimeNanos());
      }
    }, callback, SystemClock.uptimeMillis() + delayMillis);
  }

  @Implementation
  public void removeFrameCallback(Choreographer.FrameCallback callback) {
    handler.removeCallbacksAndMessages(callback);
  }

  @Implementation
  public long getFrameTimeNanos() {
    final long now = nanoTime;
    nanoTime += ShadowChoreographer.FRAME_INTERVAL;
    return now;
  }

  /**
   * Return the current inter-frame interval.
   *
   * @return  Inter-frame interval.
   */
  public static long getFrameInterval() {
    return ShadowChoreographer.FRAME_INTERVAL;
  }

  /**
   * Set the inter-frame interval used to advance the clock. By default, this is set to 1ms.
   *
   * @param frameInterval  Inter-frame interval.
   */
  public static void setFrameInterval(long frameInterval) {
    ShadowChoreographer.FRAME_INTERVAL = frameInterval;
  }

  @Resetter
  public static synchronized void reset() {
    // Blech. We need to share the main looper because somebody might refer to it in a static
    // field. We also need to keep it in a soft reference so we don't max out permgen.
    if (Thread.currentThread() != MAIN_THREAD) {
      throw new RuntimeException("You should only call this from the main thread!");
    }
    instance = makeThreadLocal();
    FRAME_INTERVAL = 10 * TimeUtils.NANOS_PER_MS; // 10ms
  }
}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy