content: /*
   * Copyright (C) 2015 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.animation;
  
  import android.os.SystemClock;
  import android.util.ArrayMap;
  import android.view.Choreographer;
  
  import java.util.ArrayList;
  
  /**
   * This custom, static handler handles the timing pulse that is shared by all active
   * ValueAnimators. This approach ensures that the setting of animation values will happen on the
   * same thread that animations start on, and that all animations will share the same times for
   * calculating their values, which makes synchronizing animations possible.
   *
   * The handler uses the Choreographer by default for doing periodic callbacks. A custom
   * AnimationFrameCallbackProvider can be set on the handler to provide timing pulse that
   * may be independent of UI frame update. This could be useful in testing.
   *
   * @hide
   */
  public class AnimationHandler {
      /**
       * Internal per-thread collections used to avoid set collisions as animations start and end
       * while being processed.
       * @hide
       */
      private final ArrayMap<AnimationFrameCallback, Long> mDelayedCallbackStartTime =
              new ArrayMap<>();
      private final ArrayList<AnimationFrameCallback> mAnimationCallbacks =
              new ArrayList<>();
      private final ArrayList<AnimationFrameCallback> mCommitCallbacks =
              new ArrayList<>();
      private AnimationFrameCallbackProvider mProvider;
  
      private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
          @Override
          public void doFrame(long frameTimeNanos) {
              doAnimationFrame(getProvider().getFrameTime());
              if (mAnimationCallbacks.size() > 0) {
                  getProvider().postFrameCallback(this);
              }
          }
      };
  
      public final static ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>();
      private boolean mListDirty = false;
  
      public static AnimationHandler getInstance() {
          if (sAnimatorHandler.get() == null) {
              sAnimatorHandler.set(new AnimationHandler());
          }
          return sAnimatorHandler.get();
      }
  
      /**
       * By default, the Choreographer is used to provide timing for frame callbacks. A custom
       * provider can be used here to provide different timing pulse.
       */
      public void setProvider(AnimationFrameCallbackProvider provider) {
          if (provider == null) {
              mProvider = new MyFrameCallbackProvider();
          } else {
              mProvider = provider;
          }
      }
  
      private AnimationFrameCallbackProvider getProvider() {
          if (mProvider == null) {
              mProvider = new MyFrameCallbackProvider();
          }
          return mProvider;
      }
  
      /**
       * Register to get a callback on the next frame after the delay.
       */
      public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
          if (mAnimationCallbacks.size() == 0) {
              getProvider().postFrameCallback(mFrameCallback);
          }
          if (!mAnimationCallbacks.contains(callback)) {
              mAnimationCallbacks.add(callback);
          }
  
          if (delay > 0) {
              mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
          }
      }
  
      /**
       * Register to get a one shot callback for frame commit timing. Frame commit timing is the
       * time *after* traversals are done, as opposed to the animation frame timing, which is
       * before any traversals. This timing can be used to adjust the start time of an animation
       * when expensive traversals create big delta between the animation frame timing and the time
       * that animation is first shown on screen.
       *
       * Note this should only be called when the animation has already registered to receive
       * animation frame callbacks. This callback will be guaranteed to happen *after* the next
       * animation frame callback.
       */
      public void addOneShotCommitCallback(final AnimationFrameCallback callback) {
          if (!mCommitCallbacks.contains(callback)) {
              mCommitCallbacks.add(callback);
          }
      }
  
      /**
       * Removes the given callback from the list, so it will no longer be called for frame related
       * timing.
       */
      public void removeCallback(AnimationFrameCallback callback) {
          mCommitCallbacks.remove(callback);
          mDelayedCallbackStartTime.remove(callback);
          int id = mAnimationCallbacks.indexOf(callback);
          if (id >= 0) {
              mAnimationCallbacks.set(id, null);
              mListDirty = true;
          }
      }
  
      private void doAnimationFrame(long frameTime) {
          long currentTime = SystemClock.uptimeMillis();
          final int size = mAnimationCallbacks.size();
          for (int i = 0; i < size; i++) {
              final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
              if (callback == null) {
                  continue;
              }
              if (isCallbackDue(callback, currentTime)) {
                  callback.doAnimationFrame(frameTime);
                  if (mCommitCallbacks.contains(callback)) {
                      getProvider().postCommitCallback(new Runnable() {
                          @Override
                          public void run() {
                              commitAnimationFrame(callback, getProvider().getFrameTime());
                          }
                      });
                  }
              }
          }
          cleanUpList();
      }
  
      private void commitAnimationFrame(AnimationFrameCallback callback, long frameTime) {
          if (!mDelayedCallbackStartTime.containsKey(callback) &&
                  mCommitCallbacks.contains(callback)) {
              callback.commitAnimationFrame(frameTime);
              mCommitCallbacks.remove(callback);
          }
      }
  
      /**
       * Remove the callbacks from mDelayedCallbackStartTime once they have passed the initial delay
       * so that they can start getting frame callbacks.
       *
       * @return true if they have passed the initial delay or have no delay, false otherwise.
       */
      private boolean isCallbackDue(AnimationFrameCallback callback, long currentTime) {
          Long startTime = mDelayedCallbackStartTime.get(callback);
          if (startTime == null) {
              return true;
          }
          if (startTime < currentTime) {
              mDelayedCallbackStartTime.remove(callback);
              return true;
          }
          return false;
      }
  
      /**
       * Return the number of callbacks that have registered for frame callbacks.
       */
      public static int getAnimationCount() {
          AnimationHandler handler = sAnimatorHandler.get();
          if (handler == null) {
              return 0;
          }
          return handler.getCallbackSize();
      }
  
      public static void setFrameDelay(long delay) {
          getInstance().getProvider().setFrameDelay(delay);
      }
  
      public static long getFrameDelay() {
          return getInstance().getProvider().getFrameDelay();
      }
  
      void autoCancelBasedOn(ObjectAnimator objectAnimator) {
          for (int i = mAnimationCallbacks.size() - 1; i >= 0; i--) {
              AnimationFrameCallback cb = mAnimationCallbacks.get(i);
              if (cb == null) {
                  continue;
              }
              if (objectAnimator.shouldAutoCancel(cb)) {
                  ((Animator) mAnimationCallbacks.get(i)).cancel();
              }
          }
      }
  
      private void cleanUpList() {
          if (mListDirty) {
              for (int i = mAnimationCallbacks.size() - 1; i >= 0; i--) {
                  if (mAnimationCallbacks.get(i) == null) {
                      mAnimationCallbacks.remove(i);
                  }
              }
              mListDirty = false;
          }
      }
  
      private int getCallbackSize() {
          int count = 0;
          int size = mAnimationCallbacks.size();
          for (int i = size - 1; i >= 0; i--) {
              if (mAnimationCallbacks.get(i) != null) {
                  count++;
              }
          }
          return count;
      }
  
      /**
       * Default provider of timing pulse that uses Choreographer for frame callbacks.
       */
      private class MyFrameCallbackProvider implements AnimationFrameCallbackProvider {
  
          final Choreographer mChoreographer = Choreographer.getInstance();
  
          @Override
          public void postFrameCallback(Choreographer.FrameCallback callback) {
              mChoreographer.postFrameCallback(callback);
          }
  
          @Override
          public void postCommitCallback(Runnable runnable) {
              mChoreographer.postCallback(Choreographer.CALLBACK_COMMIT, runnable, null);
          }
  
          @Override
          public long getFrameTime() {
              return mChoreographer.getFrameTime();
          }
  
          @Override
          public long getFrameDelay() {
              return Choreographer.getFrameDelay();
          }
  
          @Override
          public void setFrameDelay(long delay) {
              Choreographer.setFrameDelay(delay);
          }
      }
  
      /**
       * Callbacks that receives notifications for animation timing and frame commit timing.
       */
      interface AnimationFrameCallback {
          /**
           * Run animation based on the frame time.
           * @param frameTime The frame start time, in the {@link SystemClock#uptimeMillis()} time
           *                  base.
           * @return if the animation has finished.
           */
          boolean doAnimationFrame(long frameTime);
  
          /**
           * This notifies the callback of frame commit time. Frame commit time is the time after
           * traversals happen, as opposed to the normal animation frame time that is before
           * traversals. This is used to compensate expensive traversals that happen as the
           * animation starts. When traversals take a long time to complete, the rendering of the
           * initial frame will be delayed (by a long time). But since the startTime of the
           * animation is set before the traversal, by the time of next frame, a lot of time would
           * have passed since startTime was set, the animation will consequently skip a few frames
           * to respect the new frameTime. By having the commit time, we can adjust the start time to
           * when the first frame was drawn (after any expensive traversals) so that no frames
           * will be skipped.
           *
           * @param frameTime The frame time after traversals happen, if any, in the
           *                  {@link SystemClock#uptimeMillis()} time base.
           */
          void commitAnimationFrame(long frameTime);
      }
  
      /**
       * The intention for having this interface is to increase the testability of ValueAnimator.
       * Specifically, we can have a custom implementation of the interface below and provide
       * timing pulse without using Choreographer. That way we could use any arbitrary interval for
       * our timing pulse in the tests.
       *
       * @hide
       */
      public interface AnimationFrameCallbackProvider {
          void postFrameCallback(Choreographer.FrameCallback callback);
          void postCommitCallback(Runnable runnable);
          long getFrameTime();
          long getFrameDelay();
          void setFrameDelay(long delay);
      }
  }
  