content: /*
   * Copyright (C) 2016 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.internal.policy;
  
  import android.content.Context;
  import android.content.res.Configuration;
  import android.content.res.Resources;
  import android.graphics.Point;
  import android.graphics.PointF;
  import android.graphics.Rect;
  import android.util.Size;
  import android.view.Gravity;
  import android.view.ViewConfiguration;
  import android.widget.Scroller;
  
  import java.io.PrintWriter;
  import java.util.ArrayList;
  
  /**
   * Calculates the snap targets and the snap position for the PIP given a position and a velocity.
   * All bounds are relative to the display top/left.
   */
  public class PipSnapAlgorithm {
  
      // The below SNAP_MODE_* constants correspond to the config resource value
      // config_pictureInPictureSnapMode and should not be changed independently.
      // Allows snapping to the four corners
      private static final int SNAP_MODE_CORNERS_ONLY = 0;
      // Allows snapping to the four corners and the mid-points on the long edge in each orientation
      private static final int SNAP_MODE_CORNERS_AND_SIDES = 1;
      // Allows snapping to anywhere along the edge of the screen
      private static final int SNAP_MODE_EDGE = 2;
      // Allows snapping anywhere along the edge of the screen and magnets towards corners
      private static final int SNAP_MODE_EDGE_MAGNET_CORNERS = 3;
      // Allows snapping on the long edge in each orientation and magnets towards corners
      private static final int SNAP_MODE_LONG_EDGE_MAGNET_CORNERS = 4;
  
      // Threshold to magnet to a corner
      private static final float CORNER_MAGNET_THRESHOLD = 0.3f;
  
      private final Context mContext;
  
      private final ArrayList<Integer> mSnapGravities = new ArrayList<>();
      private final int mDefaultSnapMode = SNAP_MODE_EDGE_MAGNET_CORNERS;
      private int mSnapMode = mDefaultSnapMode;
  
      private final float mDefaultSizePercent;
      private final float mMinAspectRatioForMinSize;
      private final float mMaxAspectRatioForMinSize;
      private final int mFlingDeceleration;
  
      private int mOrientation = Configuration.ORIENTATION_UNDEFINED;
  
      private final int mMinimizedVisibleSize;
      private boolean mIsMinimized;
  
      public PipSnapAlgorithm(Context context) {
          Resources res = context.getResources();
          mContext = context;
          mMinimizedVisibleSize = res.getDimensionPixelSize(
                  com.android.internal.R.dimen.pip_minimized_visible_size);
          mDefaultSizePercent = res.getFloat(
                  com.android.internal.R.dimen.config_pictureInPictureDefaultSizePercent);
          mMaxAspectRatioForMinSize = res.getFloat(
                  com.android.internal.R.dimen.config_pictureInPictureAspectRatioLimitForMinSize);
          mMinAspectRatioForMinSize = 1f / mMaxAspectRatioForMinSize;
          mFlingDeceleration = mContext.getResources().getDimensionPixelSize(
                  com.android.internal.R.dimen.pip_fling_deceleration);
          onConfigurationChanged();
      }
  
      /**
       * Updates the snap algorithm when the configuration changes.
       */
      public void onConfigurationChanged() {
          Resources res = mContext.getResources();
          mOrientation = res.getConfiguration().orientation;
          mSnapMode = res.getInteger(com.android.internal.R.integer.config_pictureInPictureSnapMode);
          calculateSnapTargets();
      }
  
      /**
       * Sets the PIP's minimized state.
       */
      public void setMinimized(boolean isMinimized) {
          mIsMinimized = isMinimized;
      }
  
      /**
       * @return the closest absolute snap stack bounds for the given {@param stackBounds} moving at
       * the given {@param velocityX} and {@param velocityY}.  The {@param movementBounds} should be
       * those for the given {@param stackBounds}.
       */
      public Rect findClosestSnapBounds(Rect movementBounds, Rect stackBounds, float velocityX,
              float velocityY, Point dragStartPosition) {
          final Rect intersectStackBounds = new Rect(stackBounds);
          final Point intersect = getEdgeIntersect(stackBounds, movementBounds, velocityX, velocityY,
                  dragStartPosition);
          intersectStackBounds.offsetTo(intersect.x, intersect.y);
          return findClosestSnapBounds(movementBounds, intersectStackBounds);
      }
  
      /**
       * @return The point along the {@param movementBounds} that the PIP would intersect with based
       *         on the provided {@param velX}, {@param velY} along with the position of the PIP when
       *         the gesture started, {@param dragStartPosition}.
       */
      public Point getEdgeIntersect(Rect stackBounds, Rect movementBounds, float velX, float velY,
              Point dragStartPosition) {
          final boolean isLandscape = mOrientation == Configuration.ORIENTATION_LANDSCAPE;
          final int x = stackBounds.left;
          final int y = stackBounds.top;
  
          // Find the line of movement the PIP is on. Line defined by: y = slope * x + yIntercept
          final float slope = velY / velX; // slope = rise / run
          final float yIntercept = y - slope * x; // rearrange line equation for yIntercept
          // The PIP can have two intercept points:
          // 1) Where the line intersects with one of the edges of the screen (vertical line)
          Point vertPoint = new Point();
          // 2) Where the line intersects with the top or bottom of the screen (horizontal line)
          Point horizPoint = new Point();
  
          // Find the vertical line intersection, x will be one of the edges
          vertPoint.x = velX > 0 ? movementBounds.right : movementBounds.left;
          // Sub in x in our line equation to determine y position
          vertPoint.y = findY(slope, yIntercept, vertPoint.x);
  
          // Find the horizontal line intersection, y will be the top or bottom of the screen
          horizPoint.y = velY > 0 ? movementBounds.bottom : movementBounds.top;
          // Sub in y in our line equation to determine x position
          horizPoint.x = findX(slope, yIntercept, horizPoint.y);
  
          // Now pick one of these points -- first determine if we're flinging along the current edge.
          // Only fling along current edge if it's a direction with space for the PIP to move to
          int maxDistance;
          if (isLandscape) {
              maxDistance = velX > 0
                      ? movementBounds.right - stackBounds.left
                      : stackBounds.left - movementBounds.left;
          } else {
              maxDistance = velY > 0
                      ? movementBounds.bottom - stackBounds.top
                      : stackBounds.top - movementBounds.top;
          }
          if (maxDistance > 0) {
              // Only fling along the current edge if the start and end point are on the same side
              final int startPoint = isLandscape ? dragStartPosition.y : dragStartPosition.x;
              final int endPoint = isLandscape ? horizPoint.y : horizPoint.x;
              final int center = movementBounds.centerX();
              if ((startPoint < center && endPoint < center)
                      || (startPoint > center && endPoint > center)) {
                  // We are flinging along the current edge, figure out how far it should travel
                  // based on velocity and assumed deceleration.
                  int distance = (int) (0 - Math.pow(isLandscape ? velX : velY, 2))
                          / (2 * mFlingDeceleration);
                  distance = Math.min(distance, maxDistance);
                  // Adjust the point for the distance
                  if (isLandscape) {
                      horizPoint.x = stackBounds.left + (velX > 0 ? distance : -distance);
                  } else {
                      horizPoint.y = stackBounds.top + (velY > 0 ? distance : -distance);
                  }
                  return horizPoint;
              }
          }
          // If we're not flinging along the current edge, find the closest point instead.
          final double distanceVert = Math.hypot(vertPoint.x - x, vertPoint.y - y);
          final double distanceHoriz = Math.hypot(horizPoint.x - x, horizPoint.y - y);
          // Ensure that we're actually going somewhere
          if (distanceVert == 0) {
              return horizPoint;
          }
          if (distanceHoriz == 0) {
              return vertPoint;
          }
          // Otherwise use the closest point
          return Math.abs(distanceVert) > Math.abs(distanceHoriz) ? horizPoint : vertPoint;
      }
  
      private int findY(float slope, float yIntercept, float x) {
          return (int) ((slope * x) + yIntercept);
      }
  
      private int findX(float slope, float yIntercept, float y) {
          return (int) ((y - yIntercept) / slope);
      }
  
      /**
       * @return the closest absolute snap stack bounds for the given {@param stackBounds}.  The
       * {@param movementBounds} should be those for the given {@param stackBounds}.
       */
      public Rect findClosestSnapBounds(Rect movementBounds, Rect stackBounds) {
          final Rect pipBounds = new Rect(movementBounds.left, movementBounds.top,
                  movementBounds.right + stackBounds.width(),
                  movementBounds.bottom + stackBounds.height());
          final Rect newBounds = new Rect(stackBounds);
          if (mSnapMode == SNAP_MODE_LONG_EDGE_MAGNET_CORNERS
                  || mSnapMode == SNAP_MODE_EDGE_MAGNET_CORNERS) {
              final Rect tmpBounds = new Rect();
              final Point[] snapTargets = new Point[mSnapGravities.size()];
              for (int i = 0; i < mSnapGravities.size(); i++) {
                  Gravity.apply(mSnapGravities.get(i), stackBounds.width(), stackBounds.height(),
                          pipBounds, 0, 0, tmpBounds);
                  snapTargets[i] = new Point(tmpBounds.left, tmpBounds.top);
              }
              Point snapTarget = findClosestPoint(stackBounds.left, stackBounds.top, snapTargets);
              float distance = distanceToPoint(snapTarget, stackBounds.left, stackBounds.top);
              final float thresh = Math.max(stackBounds.width(), stackBounds.height())
                      * CORNER_MAGNET_THRESHOLD;
              if (distance < thresh) {
                  newBounds.offsetTo(snapTarget.x, snapTarget.y);
              } else {
                  snapRectToClosestEdge(stackBounds, movementBounds, newBounds);
              }
          } else if (mSnapMode == SNAP_MODE_EDGE) {
              // Find the closest edge to the given stack bounds and snap to it
              snapRectToClosestEdge(stackBounds, movementBounds, newBounds);
          } else {
              // Find the closest snap point
              final Rect tmpBounds = new Rect();
              final Point[] snapTargets = new Point[mSnapGravities.size()];
              for (int i = 0; i < mSnapGravities.size(); i++) {
                  Gravity.apply(mSnapGravities.get(i), stackBounds.width(), stackBounds.height(),
                          pipBounds, 0, 0, tmpBounds);
                  snapTargets[i] = new Point(tmpBounds.left, tmpBounds.top);
              }
              Point snapTarget = findClosestPoint(stackBounds.left, stackBounds.top, snapTargets);
              newBounds.offsetTo(snapTarget.x, snapTarget.y);
          }
          return newBounds;
      }
  
      /**
       * Applies the offset to the {@param stackBounds} to adjust it to a minimized state.
       */
      public void applyMinimizedOffset(Rect stackBounds, Rect movementBounds, Point displaySize,
              Rect stableInsets) {
          if (stackBounds.left <= movementBounds.centerX()) {
              stackBounds.offsetTo(stableInsets.left + mMinimizedVisibleSize - stackBounds.width(),
                      stackBounds.top);
          } else {
              stackBounds.offsetTo(displaySize.x - stableInsets.right - mMinimizedVisibleSize,
                      stackBounds.top);
          }
      }
  
      /**
       * @return returns a fraction that describes where along the {@param movementBounds} the
       *         {@param stackBounds} are. If the {@param stackBounds} are not currently on the
       *         {@param movementBounds} exactly, then they will be snapped to the movement bounds.
       *
       *         The fraction is defined in a clockwise fashion against the {@param movementBounds}:
       *
       *            0   1
       *          4 +---+ 1
       *            |   |
       *          3 +---+ 2
       *            3   2
       */
      public float getSnapFraction(Rect stackBounds, Rect movementBounds) {
          final Rect tmpBounds = new Rect();
          snapRectToClosestEdge(stackBounds, movementBounds, tmpBounds);
          final float widthFraction = (float) (tmpBounds.left - movementBounds.left) /
                  movementBounds.width();
          final float heightFraction = (float) (tmpBounds.top - movementBounds.top) /
                  movementBounds.height();
          if (tmpBounds.top == movementBounds.top) {
              return widthFraction;
          } else if (tmpBounds.left == movementBounds.right) {
              return 1f + heightFraction;
          } else if (tmpBounds.top == movementBounds.bottom) {
              return 2f + (1f - widthFraction);
          } else {
              return 3f + (1f - heightFraction);
          }
      }
  
      /**
       * Moves the {@param stackBounds} along the {@param movementBounds} to the given snap fraction.
       * See {@link #getSnapFraction(Rect, Rect)}.
       *
       * The fraction is define in a clockwise fashion against the {@param movementBounds}:
       *
       *    0   1
       *  4 +---+ 1
       *    |   |
       *  3 +---+ 2
       *    3   2
       */
      public void applySnapFraction(Rect stackBounds, Rect movementBounds, float snapFraction) {
          if (snapFraction < 1f) {
              int offset = movementBounds.left + (int) (snapFraction * movementBounds.width());
              stackBounds.offsetTo(offset, movementBounds.top);
          } else if (snapFraction < 2f) {
              snapFraction -= 1f;
              int offset = movementBounds.top + (int) (snapFraction * movementBounds.height());
              stackBounds.offsetTo(movementBounds.right, offset);
          } else if (snapFraction < 3f) {
              snapFraction -= 2f;
              int offset = movementBounds.left + (int) ((1f - snapFraction) * movementBounds.width());
              stackBounds.offsetTo(offset, movementBounds.bottom);
          } else {
              snapFraction -= 3f;
              int offset = movementBounds.top + (int) ((1f - snapFraction) * movementBounds.height());
              stackBounds.offsetTo(movementBounds.left, offset);
          }
      }
  
      /**
       * Adjusts {@param movementBoundsOut} so that it is the movement bounds for the given
       * {@param stackBounds}.
       */
      public void getMovementBounds(Rect stackBounds, Rect insetBounds, Rect movementBoundsOut,
              int bottomOffset) {
          // Adjust the right/bottom to ensure the stack bounds never goes offscreen
          movementBoundsOut.set(insetBounds);
          movementBoundsOut.right = Math.max(insetBounds.left, insetBounds.right -
                  stackBounds.width());
          movementBoundsOut.bottom = Math.max(insetBounds.top, insetBounds.bottom -
                  stackBounds.height());
          movementBoundsOut.bottom -= bottomOffset;
      }
  
      /**
       * @return the size of the PiP at the given {@param aspectRatio}, ensuring that the minimum edge
       * is at least {@param minEdgeSize}.
       */
      public Size getSizeForAspectRatio(float aspectRatio, float minEdgeSize, int displayWidth,
              int displayHeight) {
          final int smallestDisplaySize = Math.min(displayWidth, displayHeight);
          final int minSize = (int) Math.max(minEdgeSize, smallestDisplaySize * mDefaultSizePercent);
  
          final int width;
          final int height;
          if (aspectRatio <= mMinAspectRatioForMinSize || aspectRatio > mMaxAspectRatioForMinSize) {
              // Beyond these points, we can just use the min size as the shorter edge
              if (aspectRatio <= 1) {
                  // Portrait, width is the minimum size
                  width = minSize;
                  height = Math.round(width / aspectRatio);
              } else {
                  // Landscape, height is the minimum size
                  height = minSize;
                  width = Math.round(height * aspectRatio);
              }
          } else {
              // Within these points, we ensure that the bounds fit within the radius of the limits
              // at the points
              final float widthAtMaxAspectRatioForMinSize = mMaxAspectRatioForMinSize * minSize;
              final float radius = PointF.length(widthAtMaxAspectRatioForMinSize, minSize);
              height = (int) Math.round(Math.sqrt((radius * radius) /
                      (aspectRatio * aspectRatio + 1)));
              width = Math.round(height * aspectRatio);
          }
          return new Size(width, height);
      }
  
      /**
       * @return the closest point in {@param points} to the given {@param x} and {@param y}.
       */
      private Point findClosestPoint(int x, int y, Point[] points) {
          Point closestPoint = null;
          float minDistance = Float.MAX_VALUE;
          for (Point p : points) {
              float distance = distanceToPoint(p, x, y);
              if (distance < minDistance) {
                  closestPoint = p;
                  minDistance = distance;
              }
          }
          return closestPoint;
      }
  
      /**
       * Snaps the {@param stackBounds} to the closest edge of the {@param movementBounds} and writes
       * the new bounds out to {@param boundsOut}.
       */
      private void snapRectToClosestEdge(Rect stackBounds, Rect movementBounds, Rect boundsOut) {
          // If the stackBounds are minimized, then it should only be snapped back horizontally
          final int boundedLeft = Math.max(movementBounds.left, Math.min(movementBounds.right,
                  stackBounds.left));
          final int boundedTop = Math.max(movementBounds.top, Math.min(movementBounds.bottom,
                  stackBounds.top));
          boundsOut.set(stackBounds);
          if (mIsMinimized) {
              boundsOut.offsetTo(boundedLeft, boundedTop);
              return;
          }
  
          // Otherwise, just find the closest edge
          final int fromLeft = Math.abs(stackBounds.left - movementBounds.left);
          final int fromTop = Math.abs(stackBounds.top - movementBounds.top);
          final int fromRight = Math.abs(movementBounds.right - stackBounds.left);
          final int fromBottom = Math.abs(movementBounds.bottom - stackBounds.top);
          int shortest;
          if (mSnapMode == SNAP_MODE_LONG_EDGE_MAGNET_CORNERS) {
              // Only check longest edges
              shortest = (mOrientation == Configuration.ORIENTATION_LANDSCAPE)
                      ? Math.min(fromTop, fromBottom)
                      : Math.min(fromLeft, fromRight);
          } else {
              shortest = Math.min(Math.min(fromLeft, fromRight), Math.min(fromTop, fromBottom));
          }
          if (shortest == fromLeft) {
              boundsOut.offsetTo(movementBounds.left, boundedTop);
          } else if (shortest == fromTop) {
              boundsOut.offsetTo(boundedLeft, movementBounds.top);
          } else if (shortest == fromRight) {
              boundsOut.offsetTo(movementBounds.right, boundedTop);
          } else {
              boundsOut.offsetTo(boundedLeft, movementBounds.bottom);
          }
      }
  
      /**
       * @return the distance between point {@param p} and the given {@param x} and {@param y}.
       */
      private float distanceToPoint(Point p, int x, int y) {
          return PointF.length(p.x - x, p.y - y);
      }
  
      /**
       * Calculate the snap targets for the discrete snap modes.
       */
      private void calculateSnapTargets() {
          mSnapGravities.clear();
          switch (mSnapMode) {
              case SNAP_MODE_CORNERS_AND_SIDES:
                  if (mOrientation == Configuration.ORIENTATION_LANDSCAPE) {
                      mSnapGravities.add(Gravity.TOP | Gravity.CENTER_HORIZONTAL);
                      mSnapGravities.add(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
                  } else {
                      mSnapGravities.add(Gravity.CENTER_VERTICAL | Gravity.LEFT);
                      mSnapGravities.add(Gravity.CENTER_VERTICAL | Gravity.RIGHT);
                  }
                  // Fall through
              case SNAP_MODE_CORNERS_ONLY:
              case SNAP_MODE_EDGE_MAGNET_CORNERS:
              case SNAP_MODE_LONG_EDGE_MAGNET_CORNERS:
                  mSnapGravities.add(Gravity.TOP | Gravity.LEFT);
                  mSnapGravities.add(Gravity.TOP | Gravity.RIGHT);
                  mSnapGravities.add(Gravity.BOTTOM | Gravity.LEFT);
                  mSnapGravities.add(Gravity.BOTTOM | Gravity.RIGHT);
                  break;
              default:
                  // Skip otherwise
                  break;
          }
      }
  
      public void dump(PrintWriter pw, String prefix) {
          final String innerPrefix = prefix + "  ";
          pw.println(prefix + PipSnapAlgorithm.class.getSimpleName());
          pw.println(innerPrefix + "mSnapMode=" + mSnapMode);
          pw.println(innerPrefix + "mOrientation=" + mOrientation);
          pw.println(innerPrefix + "mMinimizedVisibleSize=" + mMinimizedVisibleSize);
          pw.println(innerPrefix + "mIsMinimized=" + mIsMinimized);
      }
  }
  