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.launcher3.graphics;
  
  import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
  
  import android.animation.Animator;
  import android.animation.AnimatorListenerAdapter;
  import android.animation.FloatArrayEvaluator;
  import android.animation.ValueAnimator;
  import android.animation.ValueAnimator.AnimatorUpdateListener;
  import android.annotation.TargetApi;
  import android.content.Context;
  import android.content.res.TypedArray;
  import android.content.res.XmlResourceParser;
  import android.graphics.Canvas;
  import android.graphics.Color;
  import android.graphics.Paint;
  import android.graphics.Path;
  import android.graphics.Rect;
  import android.graphics.Region;
  import android.graphics.Region.Op;
  import android.graphics.drawable.AdaptiveIconDrawable;
  import android.graphics.drawable.ColorDrawable;
  import android.os.Build;
  import android.util.AttributeSet;
  import android.util.SparseArray;
  import android.util.TypedValue;
  import android.util.Xml;
  import android.view.View;
  import android.view.ViewOutlineProvider;
  
  import com.android.launcher3.R;
  import com.android.launcher3.Utilities;
  import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
  import com.android.launcher3.icons.AdaptiveIconCompat;
  import com.android.launcher3.icons.GraphicsUtils;
  import com.android.launcher3.icons.IconNormalizer;
  import com.android.launcher3.util.IntArray;
  import com.android.launcher3.util.Themes;
  import com.android.launcher3.views.ClipPathView;
  
  import org.xmlpull.v1.XmlPullParser;
  import org.xmlpull.v1.XmlPullParserException;
  
  import java.io.IOException;
  import java.util.ArrayList;
  import java.util.List;
  
  import androidx.annotation.Nullable;
  
  /**
   * Abstract representation of the shape of an icon shape
   */
  public abstract class IconShape {
  
      private static IconShape sInstance = new Circle();
      private static Path sShapePath;
      private static float sNormalizationScale = ICON_VISIBLE_AREA_FACTOR;
  
      public static final int DEFAULT_PATH_SIZE = 100;
  
      public static IconShape getShape() {
          return sInstance;
      }
  
      public static Path getShapePath() {
          if (sShapePath == null) {
              Path p = new Path();
              getShape().addToPath(p, 0, 0, DEFAULT_PATH_SIZE * 0.5f);
              sShapePath = p;
          }
          return sShapePath;
      }
  
      public static float getNormalizationScale() {
          return sNormalizationScale;
      }
  
      private SparseArray<TypedValue> mAttrs;
  
      public boolean enableShapeDetection(){
          return false;
      };
  
      public abstract void drawShape(Canvas canvas, float offsetX, float offsetY, float radius,
              Paint paint);
  
      public abstract void addToPath(Path path, float offsetX, float offsetY, float radius);
  
      public abstract <T extends View & ClipPathView> Animator createRevealAnimator(T target,
              Rect startRect, Rect endRect, float endRadius, boolean isReversed);
  
      @Nullable
      public TypedValue getAttrValue(int attr) {
          return mAttrs == null ? null : mAttrs.get(attr);
      }
  
      /**
       * Abstract shape where the reveal animation is a derivative of a round rect animation
       */
      private static abstract class SimpleRectShape extends IconShape {
  
          @Override
          public final <T extends View & ClipPathView> Animator createRevealAnimator(T target,
                  Rect startRect, Rect endRect, float endRadius, boolean isReversed) {
              return new RoundedRectRevealOutlineProvider(
                      getStartRadius(startRect), endRadius, startRect, endRect) {
                  @Override
                  public boolean shouldRemoveElevationDuringAnimation() {
                      return true;
                  }
              }.createRevealAnimator(target, isReversed);
          }
  
          protected abstract float getStartRadius(Rect startRect);
      }
  
      /**
       * Abstract shape which draws using {@link Path}
       */
      private static abstract class PathShape extends IconShape {
  
          private final Path mTmpPath = new Path();
  
          @Override
          public final void drawShape(Canvas canvas, float offsetX, float offsetY, float radius,
                  Paint paint) {
              mTmpPath.reset();
              addToPath(mTmpPath, offsetX, offsetY, radius);
              canvas.drawPath(mTmpPath, paint);
          }
  
          protected abstract AnimatorUpdateListener newUpdateListener(
                  Rect startRect, Rect endRect, float endRadius, Path outPath);
  
          @Override
          public final <T extends View & ClipPathView> Animator createRevealAnimator(T target,
                  Rect startRect, Rect endRect, float endRadius, boolean isReversed) {
              Path path = new Path();
              AnimatorUpdateListener listener =
                      newUpdateListener(startRect, endRect, endRadius, path);
  
              ValueAnimator va =
                      isReversed ? ValueAnimator.ofFloat(1f, 0f) : ValueAnimator.ofFloat(0f, 1f);
              va.addListener(new AnimatorListenerAdapter() {
                  private ViewOutlineProvider mOldOutlineProvider;
  
                  public void onAnimationStart(Animator animation) {
                      mOldOutlineProvider = target.getOutlineProvider();
                      target.setOutlineProvider(null);
  
                      target.setTranslationZ(-target.getElevation());
                  }
  
                  public void onAnimationEnd(Animator animation) {
                      target.setTranslationZ(0);
                      target.setClipPath(null);
                      target.setOutlineProvider(mOldOutlineProvider);
                  }
              });
  
              va.addUpdateListener((anim) -> {
                  path.reset();
                  listener.onAnimationUpdate(anim);
                  target.setClipPath(path);
              });
  
              return va;
          }
      }
  
      public static final class Circle extends SimpleRectShape {
  
          @Override
          public void drawShape(Canvas canvas, float offsetX, float offsetY, float radius, Paint p) {
              canvas.drawCircle(radius + offsetX, radius + offsetY, radius, p);
          }
  
          @Override
          public void addToPath(Path path, float offsetX, float offsetY, float radius) {
              path.addCircle(radius + offsetX, radius + offsetY, radius, Path.Direction.CW);
          }
  
          @Override
          protected float getStartRadius(Rect startRect) {
              return startRect.width() / 2f;
          }
  
          @Override
          public boolean enableShapeDetection() {
              return true;
          }
      }
  
      public static class RoundedSquare extends SimpleRectShape {
  
          /**
           * Ratio of corner radius to half size.
           */
          private final float mRadiusRatio;
  
          public RoundedSquare(float radiusRatio) {
              mRadiusRatio = radiusRatio;
          }
  
          @Override
          public void drawShape(Canvas canvas, float offsetX, float offsetY, float radius, Paint p) {
              float cx = radius + offsetX;
              float cy = radius + offsetY;
              float cr = radius * mRadiusRatio;
              canvas.drawRoundRect(cx - radius, cy - radius, cx + radius, cy + radius, cr, cr, p);
          }
  
          @Override
          public void addToPath(Path path, float offsetX, float offsetY, float radius) {
              float cx = radius + offsetX;
              float cy = radius + offsetY;
              float cr = radius * mRadiusRatio;
              path.addRoundRect(cx - radius, cy - radius, cx + radius, cy + radius, cr, cr,
                      Path.Direction.CW);
          }
  
          @Override
          protected float getStartRadius(Rect startRect) {
              return (startRect.width() / 2f) * mRadiusRatio;
          }
      }
  
      public static class TearDrop extends PathShape {
  
          /**
           * Radio of short radius to large radius, based on the shape options defined in the config.
           */
          private final float mRadiusRatio;
          private final float[] mTempRadii = new float[8];
  
          public TearDrop(float radiusRatio) {
              mRadiusRatio = radiusRatio;
          }
  
          @Override
          public void addToPath(Path p, float offsetX, float offsetY, float r1) {
              float r2 = r1 * mRadiusRatio;
              float cx = r1 + offsetX;
              float cy = r1 + offsetY;
  
              p.addRoundRect(cx - r1, cy - r1, cx + r1, cy + r1, getRadiiArray(r1, r2),
                      Path.Direction.CW);
          }
  
          private float[] getRadiiArray(float r1, float r2) {
              mTempRadii[0] = mTempRadii [1] = mTempRadii[2] = mTempRadii[3] =
                      mTempRadii[6] = mTempRadii[7] = r1;
              mTempRadii[4] = mTempRadii[5] = r2;
              return mTempRadii;
          }
  
          @Override
          protected AnimatorUpdateListener newUpdateListener(Rect startRect, Rect endRect,
                  float endRadius, Path outPath) {
              float r1 = startRect.width() / 2f;
              float r2 = r1 * mRadiusRatio;
  
              float[] startValues = new float[] {
                      startRect.left, startRect.top, startRect.right, startRect.bottom, r1, r2};
              float[] endValues = new float[] {
                      endRect.left, endRect.top, endRect.right, endRect.bottom, endRadius, endRadius};
  
              FloatArrayEvaluator evaluator = new FloatArrayEvaluator(new float[6]);
  
              return (anim) -> {
                  float progress = (Float) anim.getAnimatedValue();
                  float[] values = evaluator.evaluate(progress, startValues, endValues);
                  outPath.addRoundRect(
                          values[0], values[1], values[2], values[3],
                          getRadiiArray(values[4], values[5]), Path.Direction.CW);
              };
          }
      }
  
      public static class Squircle extends PathShape {
  
          /**
           * Radio of radius to circle radius, based on the shape options defined in the config.
           */
          private final float mRadiusRatio;
  
          public Squircle(float radiusRatio) {
              mRadiusRatio = radiusRatio;
          }
  
          @Override
          public void addToPath(Path p, float offsetX, float offsetY, float r) {
              float cx = r + offsetX;
              float cy = r + offsetY;
              float control = r - r * mRadiusRatio;
  
              p.moveTo(cx, cy - r);
              addLeftCurve(cx, cy, r, control, p);
              addRightCurve(cx, cy, r, control, p);
              addLeftCurve(cx, cy, -r, -control, p);
              addRightCurve(cx, cy, -r, -control, p);
              p.close();
          }
  
          private void addLeftCurve(float cx, float cy, float r, float control, Path path) {
              path.cubicTo(
                      cx - control, cy - r,
                      cx - r, cy - control,
                      cx - r, cy);
          }
  
          private void addRightCurve(float cx, float cy, float r, float control, Path path) {
              path.cubicTo(
                      cx - r, cy + control,
                      cx - control, cy + r,
                      cx, cy + r);
          }
  
          @Override
          protected AnimatorUpdateListener newUpdateListener(Rect startRect, Rect endRect,
                  float endR, Path outPath) {
  
              float startCX = startRect.exactCenterX();
              float startCY = startRect.exactCenterY();
              float startR = startRect.width() / 2f;
              float startControl = startR - startR * mRadiusRatio;
              float startHShift = 0;
              float startVShift = 0;
  
              float endCX = endRect.exactCenterX();
              float endCY = endRect.exactCenterY();
              // Approximate corner circle using bezier curves
              // http://spencermortensen.com/articles/bezier-circle/
              float endControl = endR * 0.551915024494f;
              float endHShift = endRect.width() / 2f - endR;
              float endVShift = endRect.height() / 2f - endR;
  
              return (anim) -> {
                  float progress = (Float) anim.getAnimatedValue();
  
                  float cx = (1 - progress) * startCX + progress * endCX;
                  float cy = (1 - progress) * startCY + progress * endCY;
                  float r = (1 - progress) * startR + progress * endR;
                  float control = (1 - progress) * startControl + progress * endControl;
                  float hShift = (1 - progress) * startHShift + progress * endHShift;
                  float vShift = (1 - progress) * startVShift + progress * endVShift;
  
                  outPath.moveTo(cx, cy - vShift - r);
                  outPath.rLineTo(-hShift, 0);
  
                  addLeftCurve(cx - hShift, cy - vShift, r, control, outPath);
                  outPath.rLineTo(0, vShift + vShift);
  
                  addRightCurve(cx - hShift, cy + vShift, r, control, outPath);
                  outPath.rLineTo(hShift + hShift, 0);
  
                  addLeftCurve(cx + hShift, cy + vShift, -r, -control, outPath);
                  outPath.rLineTo(0, -vShift - vShift);
  
                  addRightCurve(cx + hShift, cy - vShift, -r, -control, outPath);
                  outPath.close();
              };
          }
      }
  
      /**
       * Initializes the shape which is closest to the {@link AdaptiveIconDrawable}
       */
      public static void init(Context context) {
          if (!Utilities.ATLEAST_OREO) {
              return;
          }
          pickBestShape(context);
      }
  
      private static IconShape getShapeDefinition(String type, float radius) {
          switch (type) {
              case "Circle":
                  return new Circle();
              case "RoundedSquare":
                  return new RoundedSquare(radius);
              case "TearDrop":
                  return new TearDrop(radius);
              case "Squircle":
                  return new Squircle(radius);
              default:
                  throw new IllegalArgumentException("Invalid shape type: " + type);
          }
      }
  
      private static List<IconShape> getAllShapes(Context context) {
          ArrayList<IconShape> result = new ArrayList<>();
          try (XmlResourceParser parser = context.getResources().getXml(R.xml.folder_shapes)) {
  
              // Find the root tag
              int type;
              while ((type = parser.next()) != XmlPullParser.END_TAG
                      && type != XmlPullParser.END_DOCUMENT
                      && !"shapes".equals(parser.getName()));
  
              final int depth = parser.getDepth();
              int[] radiusAttr = new int[] {R.attr.folderIconRadius};
              IntArray keysToIgnore = new IntArray(0);
  
              while (((type = parser.next()) != XmlPullParser.END_TAG ||
                      parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
  
                  if (type == XmlPullParser.START_TAG) {
                      AttributeSet attrs = Xml.asAttributeSet(parser);
                      TypedArray a = context.obtainStyledAttributes(attrs, radiusAttr);
                      IconShape shape = getShapeDefinition(parser.getName(), a.getFloat(0, 1));
                      a.recycle();
  
                      shape.mAttrs = Themes.createValueMap(context, attrs, keysToIgnore);
                      result.add(shape);
                  }
              }
          } catch (IOException | XmlPullParserException e) {
              throw new RuntimeException(e);
          }
          return result;
      }
  
      @TargetApi(Build.VERSION_CODES.O)
      protected static void pickBestShape(Context context) {
          // Pick any large size
          final int size = 200;
  
          Region full = new Region(0, 0, size, size);
          Region iconR = new Region();
          AdaptiveIconDrawable drawable = AdaptiveIconCompat.wrap(new AdaptiveIconDrawable(
                  new ColorDrawable(Color.BLACK), new ColorDrawable(Color.BLACK)));
          drawable.setBounds(0, 0, size, size);
          iconR.setPath(drawable.getIconMask(), full);
  
          Path shapePath = new Path();
          Region shapeR = new Region();
  
          // Find the shape with minimum area of divergent region.
          int minArea = Integer.MAX_VALUE;
          IconShape closestShape = null;
          for (IconShape shape : getAllShapes(context)) {
              shapePath.reset();
              shape.addToPath(shapePath, 0, 0, size / 2f);
              shapeR.setPath(shapePath, full);
              shapeR.op(iconR, Op.XOR);
  
              int area = GraphicsUtils.getArea(shapeR);
              if (area < minArea) {
                  minArea = area;
                  closestShape = shape;
              }
          }
  
          if (closestShape != null) {
              sInstance = closestShape;
          }
  
          // Initialize shape properties
          drawable.setBounds(0, 0, DEFAULT_PATH_SIZE, DEFAULT_PATH_SIZE);
          sShapePath = new Path(drawable.getIconMask());
          sNormalizationScale = IconNormalizer.normalizeAdaptiveIcon(drawable, size, null);
      }
  }
  