content: /*
   * Copyright (C) 2018 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 android.graphics.Matrix;
  import android.graphics.Rect;
  import android.os.SystemClock;
  import android.util.proto.ProtoOutputStream;
  import android.view.DisplayInfo;
  import android.view.SurfaceControl;
  import android.view.SurfaceControl.Transaction;
  import android.view.animation.AlphaAnimation;
  import android.view.animation.Animation;
  import android.view.animation.AnimationSet;
  import android.view.animation.ClipRectAnimation;
  import android.view.animation.ScaleAnimation;
  import android.view.animation.Transformation;
  import android.view.animation.TranslateAnimation;
  
  import com.android.server.wm.LocalAnimationAdapter.AnimationSpec;
  
  import java.io.PrintWriter;
  
  /**
   * Animation spec for changing window animations.
   */
  public class WindowChangeAnimationSpec implements AnimationSpec {
  
      private final ThreadLocal<TmpValues> mThreadLocalTmps = ThreadLocal.withInitial(TmpValues::new);
      private final boolean mIsAppAnimation;
      private final Rect mStartBounds;
      private final Rect mEndBounds;
      private final Rect mTmpRect = new Rect();
  
      private Animation mAnimation;
      private final boolean mIsThumbnail;
  
      static final int ANIMATION_DURATION = AppTransition.DEFAULT_APP_TRANSITION_DURATION;
  
      public WindowChangeAnimationSpec(Rect startBounds, Rect endBounds, DisplayInfo displayInfo,
              float durationScale, boolean isAppAnimation, boolean isThumbnail) {
          mStartBounds = new Rect(startBounds);
          mEndBounds = new Rect(endBounds);
          mIsAppAnimation = isAppAnimation;
          mIsThumbnail = isThumbnail;
          createBoundsInterpolator((int) (ANIMATION_DURATION * durationScale), displayInfo);
      }
  
      @Override
      public boolean getShowWallpaper() {
          return false;
      }
  
      @Override
      public int getBackgroundColor() {
          return 0;
      }
  
      @Override
      public long getDuration() {
          return mAnimation.getDuration();
      }
  
      /**
       * This animator behaves slightly differently depending on whether the window is growing
       * or shrinking:
       * If growing, it will do a clip-reveal after quicker fade-out/scale of the smaller (old)
       * snapshot.
       * If shrinking, it will do an opposite clip-reveal on the old snapshot followed by a quicker
       * fade-out of the bigger (old) snapshot while simultaneously shrinking the new window into
       * place.
       * @param duration
       * @param displayInfo
       */
      private void createBoundsInterpolator(long duration, DisplayInfo displayInfo) {
          boolean growing = mEndBounds.width() - mStartBounds.width()
                  + mEndBounds.height() - mStartBounds.height() >= 0;
          float scalePart = 0.7f;
          long scalePeriod = (long) (duration * scalePart);
          float startScaleX = scalePart * ((float) mStartBounds.width()) / mEndBounds.width()
                  + (1.f - scalePart);
          float startScaleY = scalePart * ((float) mStartBounds.height()) / mEndBounds.height()
                  + (1.f - scalePart);
          if (mIsThumbnail) {
              AnimationSet animSet = new AnimationSet(true);
              Animation anim = new AlphaAnimation(1.f, 0.f);
              anim.setDuration(scalePeriod);
              if (!growing) {
                  anim.setStartOffset(duration - scalePeriod);
              }
              animSet.addAnimation(anim);
              float endScaleX = 1.f / startScaleX;
              float endScaleY = 1.f / startScaleY;
              anim = new ScaleAnimation(endScaleX, endScaleX, endScaleY, endScaleY);
              anim.setDuration(duration);
              animSet.addAnimation(anim);
              mAnimation = animSet;
              mAnimation.initialize(mStartBounds.width(), mStartBounds.height(),
                      mEndBounds.width(), mEndBounds.height());
          } else {
              AnimationSet animSet = new AnimationSet(true);
              final Animation scaleAnim = new ScaleAnimation(startScaleX, 1, startScaleY, 1);
              scaleAnim.setDuration(scalePeriod);
              if (!growing) {
                  scaleAnim.setStartOffset(duration - scalePeriod);
              }
              animSet.addAnimation(scaleAnim);
              final Animation translateAnim = new TranslateAnimation(mStartBounds.left,
                      mEndBounds.left, mStartBounds.top, mEndBounds.top);
              translateAnim.setDuration(duration);
              animSet.addAnimation(translateAnim);
              Rect startClip = new Rect(mStartBounds);
              Rect endClip = new Rect(mEndBounds);
              startClip.offsetTo(0, 0);
              endClip.offsetTo(0, 0);
              final Animation clipAnim = new ClipRectAnimation(startClip, endClip);
              clipAnim.setDuration(duration);
              animSet.addAnimation(clipAnim);
              mAnimation = animSet;
              mAnimation.initialize(mStartBounds.width(), mStartBounds.height(),
                      displayInfo.appWidth, displayInfo.appHeight);
          }
      }
  
      @Override
      public void apply(Transaction t, SurfaceControl leash, long currentPlayTime) {
          final TmpValues tmp = mThreadLocalTmps.get();
          if (mIsThumbnail) {
              mAnimation.getTransformation(currentPlayTime, tmp.mTransformation);
              t.setMatrix(leash, tmp.mTransformation.getMatrix(), tmp.mFloats);
              t.setAlpha(leash, tmp.mTransformation.getAlpha());
          } else {
              mAnimation.getTransformation(currentPlayTime, tmp.mTransformation);
              final Matrix matrix = tmp.mTransformation.getMatrix();
              t.setMatrix(leash, matrix, tmp.mFloats);
  
              // The following applies an inverse scale to the clip-rect so that it crops "after" the
              // scale instead of before.
              tmp.mVecs[1] = tmp.mVecs[2] = 0;
              tmp.mVecs[0] = tmp.mVecs[3] = 1;
              matrix.mapVectors(tmp.mVecs);
              tmp.mVecs[0] = 1.f / tmp.mVecs[0];
              tmp.mVecs[3] = 1.f / tmp.mVecs[3];
              final Rect clipRect = tmp.mTransformation.getClipRect();
              mTmpRect.left = (int) (clipRect.left * tmp.mVecs[0] + 0.5f);
              mTmpRect.right = (int) (clipRect.right * tmp.mVecs[0] + 0.5f);
              mTmpRect.top = (int) (clipRect.top * tmp.mVecs[3] + 0.5f);
              mTmpRect.bottom = (int) (clipRect.bottom * tmp.mVecs[3] + 0.5f);
              t.setWindowCrop(leash, mTmpRect);
          }
      }
  
      @Override
      public long calculateStatusBarTransitionStartTime() {
          long uptime = SystemClock.uptimeMillis();
          return Math.max(uptime, uptime + ((long) (((float) mAnimation.getDuration()) * 0.99f))
                  - STATUS_BAR_TRANSITION_DURATION);
      }
  
      @Override
      public boolean canSkipFirstFrame() {
          return false;
      }
  
      @Override
      public boolean needsEarlyWakeup() {
          return mIsAppAnimation;
      }
  
      @Override
      public void dump(PrintWriter pw, String prefix) {
          pw.print(prefix); pw.println(mAnimation.getDuration());
      }
  
      @Override
      public void writeToProtoInner(ProtoOutputStream proto) {
          final long token = proto.start(WINDOW);
          proto.write(ANIMATION, mAnimation.toString());
          proto.end(token);
      }
  
      private static class TmpValues {
          final Transformation mTransformation = new Transformation();
          final float[] mFloats = new float[9];
          final float[] mVecs = new float[4];
      }
  }
  