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.AlphaAnimationSpecProto.DURATION_MS;
  import static com.android.server.wm.AlphaAnimationSpecProto.FROM;
  import static com.android.server.wm.AlphaAnimationSpecProto.TO;
  import static com.android.server.wm.AnimationSpecProto.ALPHA;
  
  import android.graphics.Rect;
  import android.util.Log;
  import android.util.proto.ProtoOutputStream;
  import android.view.Surface;
  import android.view.SurfaceControl;
  
  import com.android.internal.annotations.VisibleForTesting;
  
  import java.io.PrintWriter;
  
  /**
   * Utility class for use by a WindowContainer implementation to add "DimLayer" support, that is
   * black layers of varying opacity at various Z-levels which create the effect of a Dim.
   */
  class Dimmer {
      private static final String TAG = "WindowManager";
      // This is in milliseconds.
      private static final int DEFAULT_DIM_ANIM_DURATION = 200;
  
      private class DimAnimatable implements SurfaceAnimator.Animatable {
          private SurfaceControl mDimLayer;
  
          private DimAnimatable(SurfaceControl dimLayer) {
              mDimLayer = dimLayer;
          }
  
          @Override
          public SurfaceControl.Transaction getPendingTransaction() {
              return mHost.getPendingTransaction();
          }
  
          @Override
          public void commitPendingTransaction() {
              mHost.commitPendingTransaction();
          }
  
          @Override
          public void onAnimationLeashCreated(SurfaceControl.Transaction t, SurfaceControl leash) {
          }
  
          @Override
          public void onAnimationLeashLost(SurfaceControl.Transaction t) {
          }
  
          @Override
          public SurfaceControl.Builder makeAnimationLeash() {
              return mHost.makeAnimationLeash();
          }
  
          @Override
          public SurfaceControl getAnimationLeashParent() {
              return mHost.getSurfaceControl();
          }
  
          @Override
          public SurfaceControl getSurfaceControl() {
              return mDimLayer;
          }
  
          @Override
          public SurfaceControl getParentSurfaceControl() {
              return mHost.getSurfaceControl();
          }
  
          @Override
          public int getSurfaceWidth() {
              // This will determine the size of the leash created. This should be the size of the
              // host and not the dim layer since the dim layer may get bigger during animation. If
              // that occurs, the leash size cannot change so we need to ensure the leash is big
              // enough that the dim layer can grow.
              // This works because the mHost will be a Task which has the display bounds.
              return mHost.getSurfaceWidth();
          }
  
          @Override
          public int getSurfaceHeight() {
              // See getSurfaceWidth() above for explanation.
              return mHost.getSurfaceHeight();
          }
  
          void removeSurface() {
              if (mDimLayer != null && mDimLayer.isValid()) {
                  getPendingTransaction().remove(mDimLayer);
              }
              mDimLayer = null;
          }
      }
  
      @VisibleForTesting
      class DimState {
          /**
           * The layer where property changes should be invoked on.
           */
          SurfaceControl mDimLayer;
          boolean mDimming;
          boolean isVisible;
          SurfaceAnimator mSurfaceAnimator;
  
          /**
           * Determines whether the dim layer should animate before destroying.
           */
          boolean mAnimateExit = true;
  
          /**
           * Used for Dims not associated with a WindowContainer. See {@link Dimmer#dimAbove} for
           * details on Dim lifecycle.
           */
          boolean mDontReset;
  
          DimState(SurfaceControl dimLayer) {
              mDimLayer = dimLayer;
              mDimming = true;
              final DimAnimatable dimAnimatable = new DimAnimatable(dimLayer);
              mSurfaceAnimator = new SurfaceAnimator(dimAnimatable, () -> {
                  if (!mDimming) {
                      dimAnimatable.removeSurface();
                  }
              }, mHost.mWmService);
          }
      }
  
      /**
       * The {@link WindowContainer} that our Dim's are bounded to. We may be dimming on behalf of the
       * host, some controller of it, or one of the hosts children.
       */
      private WindowContainer mHost;
      private WindowContainer mLastRequestedDimContainer;
      @VisibleForTesting
      DimState mDimState;
  
      private final SurfaceAnimatorStarter mSurfaceAnimatorStarter;
  
      @VisibleForTesting
      interface SurfaceAnimatorStarter {
          void startAnimation(SurfaceAnimator surfaceAnimator, SurfaceControl.Transaction t,
                  AnimationAdapter anim, boolean hidden);
      }
  
      Dimmer(WindowContainer host) {
          this(host, SurfaceAnimator::startAnimation);
      }
  
      Dimmer(WindowContainer host, SurfaceAnimatorStarter surfaceAnimatorStarter) {
          mHost = host;
          mSurfaceAnimatorStarter = surfaceAnimatorStarter;
      }
  
      private SurfaceControl makeDimLayer() {
          return mHost.makeChildSurface(null)
                  .setParent(mHost.getSurfaceControl())
                  .setColorLayer()
                  .setName("Dim Layer for - " + mHost.getName())
                  .build();
      }
  
      /**
       * Retrieve the DimState, creating one if it doesn't exist.
       */
      private DimState getDimState(WindowContainer container) {
          if (mDimState == null) {
              try {
                  final SurfaceControl ctl = makeDimLayer();
                  mDimState = new DimState(ctl);
                  /**
                   * See documentation on {@link #dimAbove} to understand lifecycle management of
                   * Dim's via state resetting for Dim's with containers.
                   */
                  if (container == null) {
                      mDimState.mDontReset = true;
                  }
              } catch (Surface.OutOfResourcesException e) {
                  Log.w(TAG, "OutOfResourcesException creating dim surface");
              }
          }
  
          mLastRequestedDimContainer = container;
          return mDimState;
      }
  
      private void dim(SurfaceControl.Transaction t, WindowContainer container, int relativeLayer,
              float alpha) {
          final DimState d = getDimState(container);
  
          if (d == null) {
              return;
          }
  
          if (container != null) {
              // The dim method is called from WindowState.prepareSurfaces(), which is always called
              // in the correct Z from lowest Z to highest. This ensures that the dim layer is always
              // relative to the highest Z layer with a dim.
              t.setRelativeLayer(d.mDimLayer, container.getSurfaceControl(), relativeLayer);
          } else {
              t.setLayer(d.mDimLayer, Integer.MAX_VALUE);
          }
          t.setAlpha(d.mDimLayer, alpha);
  
          d.mDimming = true;
      }
  
      /**
       * Finish a dim started by dimAbove in the case there was no call to dimAbove.
       *
       * @param t A Transaction in which to finish the dim.
       */
      void stopDim(SurfaceControl.Transaction t) {
          if (mDimState != null) {
              t.hide(mDimState.mDimLayer);
              mDimState.isVisible = false;
              mDimState.mDontReset = false;
          }
      }
  
      /**
       * Place a Dim above the entire host container. The caller is responsible for calling stopDim to
       * remove this effect. If the Dim can be assosciated with a particular child of the host
       * consider using the other variant of dimAbove which ties the Dim lifetime to the child
       * lifetime more explicitly.
       *
       * @param t     A transaction in which to apply the Dim.
       * @param alpha The alpha at which to Dim.
       */
      void dimAbove(SurfaceControl.Transaction t, float alpha) {
          dim(t, null, 1, alpha);
      }
  
      /**
       * Place a dim above the given container, which should be a child of the host container.
       * for each call to {@link WindowContainer#prepareSurfaces} the Dim state will be reset
       * and the child should call dimAbove again to request the Dim to continue.
       *
       * @param t         A transaction in which to apply the Dim.
       * @param container The container which to dim above. Should be a child of our host.
       * @param alpha     The alpha at which to Dim.
       */
      void dimAbove(SurfaceControl.Transaction t, WindowContainer container, float alpha) {
          dim(t, container, 1, alpha);
      }
  
      /**
       * Like {@link #dimAbove} but places the dim below the given container.
       *
       * @param t         A transaction in which to apply the Dim.
       * @param container The container which to dim below. Should be a child of our host.
       * @param alpha     The alpha at which to Dim.
       */
  
      void dimBelow(SurfaceControl.Transaction t, WindowContainer container, float alpha) {
          dim(t, container, -1, alpha);
      }
  
      /**
       * Mark all dims as pending completion on the next call to {@link #updateDims}
       *
       * This is intended for us by the host container, to be called at the beginning of
       * {@link WindowContainer#prepareSurfaces}. After calling this, the container should
       * chain {@link WindowContainer#prepareSurfaces} down to it's children to give them
       * a chance to request dims to continue.
       */
      void resetDimStates() {
          if (mDimState != null && !mDimState.mDontReset) {
              mDimState.mDimming = false;
          }
      }
  
      void dontAnimateExit() {
          if (mDimState != null) {
              mDimState.mAnimateExit = false;
          }
      }
  
      /**
       * Call after invoking {@link WindowContainer#prepareSurfaces} on children as
       * described in {@link #resetDimStates}.
       *
       * @param t      A transaction in which to update the dims.
       * @param bounds The bounds at which to dim.
       * @return true if any Dims were updated.
       */
      boolean updateDims(SurfaceControl.Transaction t, Rect bounds) {
          if (mDimState == null) {
              return false;
          }
  
          if (!mDimState.mDimming) {
              if (!mDimState.mAnimateExit) {
                  if (mDimState.mDimLayer.isValid()) {
                      t.remove(mDimState.mDimLayer);
                  }
              } else {
                  startDimExit(mLastRequestedDimContainer, mDimState.mSurfaceAnimator, t);
              }
              mDimState = null;
              return false;
          } else {
              // TODO: Once we use geometry from hierarchy this falls away.
              t.setPosition(mDimState.mDimLayer, bounds.left, bounds.top);
              t.setWindowCrop(mDimState.mDimLayer, bounds.width(), bounds.height());
              if (!mDimState.isVisible) {
                  mDimState.isVisible = true;
                  t.show(mDimState.mDimLayer);
                  startDimEnter(mLastRequestedDimContainer, mDimState.mSurfaceAnimator, t);
              }
              return true;
          }
      }
  
      private void startDimEnter(WindowContainer container, SurfaceAnimator animator,
              SurfaceControl.Transaction t) {
          startAnim(container, animator, t, 0 /* startAlpha */, 1 /* endAlpha */);
      }
  
      private void startDimExit(WindowContainer container, SurfaceAnimator animator,
              SurfaceControl.Transaction t) {
          startAnim(container, animator, t, 1 /* startAlpha */, 0 /* endAlpha */);
      }
  
      private void startAnim(WindowContainer container, SurfaceAnimator animator,
              SurfaceControl.Transaction t, float startAlpha, float endAlpha) {
          mSurfaceAnimatorStarter.startAnimation(animator, t, new LocalAnimationAdapter(
                  new AlphaAnimationSpec(startAlpha, endAlpha, getDimDuration(container)),
                  mHost.mWmService.mSurfaceAnimationRunner), false /* hidden */);
      }
  
      private long getDimDuration(WindowContainer container) {
          // If there's no container, then there isn't an animation occurring while dimming. Set the
          // duration to 0 so it immediately dims to the set alpha.
          if (container == null) {
              return 0;
          }
  
          // Otherwise use the same duration as the animation on the WindowContainer
          AnimationAdapter animationAdapter = container.mSurfaceAnimator.getAnimation();
          return animationAdapter == null ? DEFAULT_DIM_ANIM_DURATION
                  : animationAdapter.getDurationHint();
      }
  
      private static class AlphaAnimationSpec implements LocalAnimationAdapter.AnimationSpec {
          private final long mDuration;
          private final float mFromAlpha;
          private final float mToAlpha;
  
          AlphaAnimationSpec(float fromAlpha, float toAlpha, long duration) {
              mFromAlpha = fromAlpha;
              mToAlpha = toAlpha;
              mDuration = duration;
          }
  
          @Override
          public long getDuration() {
              return mDuration;
          }
  
          @Override
          public void apply(SurfaceControl.Transaction t, SurfaceControl sc, long currentPlayTime) {
              float alpha = ((float) currentPlayTime / getDuration()) * (mToAlpha - mFromAlpha)
                      + mFromAlpha;
              t.setAlpha(sc, alpha);
          }
  
          @Override
          public void dump(PrintWriter pw, String prefix) {
              pw.print(prefix); pw.print("from="); pw.print(mFromAlpha);
              pw.print(" to="); pw.print(mToAlpha);
              pw.print(" duration="); pw.println(mDuration);
          }
  
          @Override
          public void writeToProtoInner(ProtoOutputStream proto) {
              final long token = proto.start(ALPHA);
              proto.write(FROM, mFromAlpha);
              proto.write(TO, mToAlpha);
              proto.write(DURATION_MS, mDuration);
              proto.end(token);
          }
      }
  }
  