content: /*
   * Copyright (C) 2017 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 com.android.server.wm;
  
  import static com.android.server.wm.AnimationAdapter.STATUS_BAR_TRANSITION_DURATION;
  import static com.android.server.wm.AnimationSpecProto.WINDOW;
  import static com.android.server.wm.WindowAnimationSpecProto.ANIMATION;
  import static com.android.server.wm.WindowStateAnimator.STACK_CLIP_AFTER_ANIM;
  import static com.android.server.wm.WindowStateAnimator.STACK_CLIP_NONE;
  
  import android.graphics.Point;
  import android.graphics.Rect;
  import android.os.SystemClock;
  import android.util.proto.ProtoOutputStream;
  import android.view.SurfaceControl;
  import android.view.SurfaceControl.Transaction;
  import android.view.animation.Animation;
  import android.view.animation.AnimationSet;
  import android.view.animation.Interpolator;
  import android.view.animation.Transformation;
  import android.view.animation.TranslateAnimation;
  
  import com.android.server.wm.LocalAnimationAdapter.AnimationSpec;
  
  import java.io.PrintWriter;
  
  /**
   * Animation spec for regular window animations.
   */
  public class WindowAnimationSpec implements AnimationSpec {
  
      private Animation mAnimation;
      private final Point mPosition = new Point();
      private final ThreadLocal<TmpValues> mThreadLocalTmps = ThreadLocal.withInitial(TmpValues::new);
      private final boolean mCanSkipFirstFrame;
      private final boolean mIsAppAnimation;
      private final Rect mStackBounds = new Rect();
      private int mStackClipMode;
      private final Rect mTmpRect = new Rect();
      private final float mWindowCornerRadius;
  
      public WindowAnimationSpec(Animation animation, Point position, boolean canSkipFirstFrame,
              float windowCornerRadius)  {
          this(animation, position, null /* stackBounds */, canSkipFirstFrame, STACK_CLIP_NONE,
                  false /* isAppAnimation */, windowCornerRadius);
      }
  
      public WindowAnimationSpec(Animation animation, Point position, Rect stackBounds,
              boolean canSkipFirstFrame, int stackClipMode, boolean isAppAnimation,
              float windowCornerRadius) {
          mAnimation = animation;
          if (position != null) {
              mPosition.set(position.x, position.y);
          }
          mWindowCornerRadius = windowCornerRadius;
          mCanSkipFirstFrame = canSkipFirstFrame;
          mIsAppAnimation = isAppAnimation;
          mStackClipMode = stackClipMode;
          if (stackBounds != null) {
              mStackBounds.set(stackBounds);
          }
      }
  
      @Override
      public boolean getShowWallpaper() {
          return mAnimation.getShowWallpaper();
      }
  
      @Override
      public int getBackgroundColor() {
          return mAnimation.getBackgroundColor();
      }
  
      @Override
      public long getDuration() {
          return mAnimation.computeDurationHint();
      }
  
      @Override
      public void apply(Transaction t, SurfaceControl leash, long currentPlayTime) {
          final TmpValues tmp = mThreadLocalTmps.get();
          tmp.transformation.clear();
          mAnimation.getTransformation(currentPlayTime, tmp.transformation);
          tmp.transformation.getMatrix().postTranslate(mPosition.x, mPosition.y);
          t.setMatrix(leash, tmp.transformation.getMatrix(), tmp.floats);
          t.setAlpha(leash, tmp.transformation.getAlpha());
  
          boolean cropSet = false;
          if (mStackClipMode == STACK_CLIP_NONE) {
              if (tmp.transformation.hasClipRect()) {
                  t.setWindowCrop(leash, tmp.transformation.getClipRect());
                  cropSet = true;
              }
          } else {
              mTmpRect.set(mStackBounds);
              if (tmp.transformation.hasClipRect()) {
                  mTmpRect.intersect(tmp.transformation.getClipRect());
              }
              t.setWindowCrop(leash, mTmpRect);
              cropSet = true;
          }
  
          // We can only apply rounded corner if a crop is set, as otherwise the value is meaningless,
          // since it doesn't have anything it's relative to.
          if (cropSet && mAnimation.hasRoundedCorners() && mWindowCornerRadius > 0) {
              t.setCornerRadius(leash, mWindowCornerRadius);
          }
      }
  
      @Override
      public long calculateStatusBarTransitionStartTime() {
          TranslateAnimation openTranslateAnimation = findTranslateAnimation(mAnimation);
          if (openTranslateAnimation != null) {
  
              // Some interpolators are extremely quickly mostly finished, but not completely. For
              // our purposes, we need to find the fraction for which ther interpolator is mostly
              // there, and use that value for the calculation.
              float t = findAlmostThereFraction(openTranslateAnimation.getInterpolator());
              return SystemClock.uptimeMillis()
                      + openTranslateAnimation.getStartOffset()
                      + (long)(openTranslateAnimation.getDuration() * t)
                      - STATUS_BAR_TRANSITION_DURATION;
          } else {
              return SystemClock.uptimeMillis();
          }
      }
  
      @Override
      public boolean canSkipFirstFrame() {
          return mCanSkipFirstFrame;
      }
  
      @Override
      public boolean needsEarlyWakeup() {
          return mIsAppAnimation;
      }
  
      @Override
      public void dump(PrintWriter pw, String prefix) {
          pw.print(prefix); pw.println(mAnimation);
      }
  
      @Override
      public void writeToProtoInner(ProtoOutputStream proto) {
          final long token = proto.start(WINDOW);
          proto.write(ANIMATION, mAnimation.toString());
          proto.end(token);
      }
  
      /**
       * Tries to find a {@link TranslateAnimation} inside the {@code animation}.
       *
       * @return the found animation, {@code null} otherwise
       */
      private static TranslateAnimation findTranslateAnimation(Animation animation) {
          if (animation instanceof TranslateAnimation) {
              return (TranslateAnimation) animation;
          } else if (animation instanceof AnimationSet) {
              AnimationSet set = (AnimationSet) animation;
              for (int i = 0; i < set.getAnimations().size(); i++) {
                  Animation a = set.getAnimations().get(i);
                  if (a instanceof TranslateAnimation) {
                      return (TranslateAnimation) a;
                  }
              }
          }
          return null;
      }
  
      /**
       * Binary searches for a {@code t} such that there exists a {@code -0.01 < eps < 0.01} for which
       * {@code interpolator(t + eps) > 0.99}.
       */
      private static float findAlmostThereFraction(Interpolator interpolator) {
          float val = 0.5f;
          float adj = 0.25f;
          while (adj >= 0.01f) {
              if (interpolator.getInterpolation(val) < 0.99f) {
                  val += adj;
              } else {
                  val -= adj;
              }
              adj /= 2;
          }
          return val;
      }
  
      private static class TmpValues {
          final Transformation transformation = new Transformation();
          final float[] floats = new float[9];
      }
  }
  