content: /*
   * Copyright (C) 2015 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 static android.view.WindowManager.DOCKED_INVALID;
  import static android.view.WindowManager.DOCKED_LEFT;
  import static android.view.WindowManager.DOCKED_RIGHT;
  
  import android.content.Context;
  import android.content.res.Configuration;
  import android.content.res.Resources;
  import android.graphics.Rect;
  import android.hardware.display.DisplayManager;
  import android.view.Display;
  import android.view.DisplayInfo;
  
  import java.util.ArrayList;
  
  /**
   * Calculates the snap targets and the snap position given a position and a velocity. All positions
   * here are to be interpreted as the left/top edge of the divider rectangle.
   *
   * @hide
   */
  public class DividerSnapAlgorithm {
  
      private static final int MIN_FLING_VELOCITY_DP_PER_SECOND = 400;
      private static final int MIN_DISMISS_VELOCITY_DP_PER_SECOND = 600;
  
      /**
       * 3 snap targets: left/top has 16:9 ratio (for videos), 1:1, and right/bottom has 16:9 ratio
       */
      private static final int SNAP_MODE_16_9 = 0;
  
      /**
       * 3 snap targets: fixed ratio, 1:1, (1 - fixed ratio)
       */
      private static final int SNAP_FIXED_RATIO = 1;
  
      /**
       * 1 snap target: 1:1
       */
      private static final int SNAP_ONLY_1_1 = 2;
  
      /**
       * 1 snap target: minimized height, (1 - minimized height)
       */
      private static final int SNAP_MODE_MINIMIZED = 3;
  
      private final float mMinFlingVelocityPxPerSecond;
      private final float mMinDismissVelocityPxPerSecond;
      private final int mDisplayWidth;
      private final int mDisplayHeight;
      private final int mDividerSize;
      private final ArrayList<SnapTarget> mTargets = new ArrayList<>();
      private final Rect mInsets = new Rect();
      private final int mSnapMode;
      private final int mMinimalSizeResizableTask;
      private final int mTaskHeightInMinimizedMode;
      private final float mFixedRatio;
      private boolean mIsHorizontalDivision;
  
      /** The first target which is still splitting the screen */
      private final SnapTarget mFirstSplitTarget;
  
      /** The last target which is still splitting the screen */
      private final SnapTarget mLastSplitTarget;
  
      private final SnapTarget mDismissStartTarget;
      private final SnapTarget mDismissEndTarget;
      private final SnapTarget mMiddleTarget;
  
      public static DividerSnapAlgorithm create(Context ctx, Rect insets) {
          DisplayInfo displayInfo = new DisplayInfo();
          ctx.getSystemService(DisplayManager.class).getDisplay(
                  Display.DEFAULT_DISPLAY).getDisplayInfo(displayInfo);
          int dividerWindowWidth = ctx.getResources().getDimensionPixelSize(
                  com.android.internal.R.dimen.docked_stack_divider_thickness);
          int dividerInsets = ctx.getResources().getDimensionPixelSize(
                  com.android.internal.R.dimen.docked_stack_divider_insets);
          return new DividerSnapAlgorithm(ctx.getResources(),
                  displayInfo.logicalWidth, displayInfo.logicalHeight,
                  dividerWindowWidth - 2 * dividerInsets,
                  ctx.getApplicationContext().getResources().getConfiguration().orientation
                          == Configuration.ORIENTATION_PORTRAIT,
                  insets);
      }
  
      public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize,
              boolean isHorizontalDivision, Rect insets) {
          this(res, displayWidth, displayHeight, dividerSize, isHorizontalDivision, insets,
                  DOCKED_INVALID, false);
      }
  
      public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize,
          boolean isHorizontalDivision, Rect insets, int dockSide) {
          this(res, displayWidth, displayHeight, dividerSize, isHorizontalDivision, insets,
              dockSide, false);
      }
  
      public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize,
              boolean isHorizontalDivision, Rect insets, int dockSide, boolean isMinimizedMode) {
          mMinFlingVelocityPxPerSecond =
                  MIN_FLING_VELOCITY_DP_PER_SECOND * res.getDisplayMetrics().density;
          mMinDismissVelocityPxPerSecond =
                  MIN_DISMISS_VELOCITY_DP_PER_SECOND * res.getDisplayMetrics().density;
          mDividerSize = dividerSize;
          mDisplayWidth = displayWidth;
          mDisplayHeight = displayHeight;
          mIsHorizontalDivision = isHorizontalDivision;
          mInsets.set(insets);
          mSnapMode = isMinimizedMode ? SNAP_MODE_MINIMIZED :
                  res.getInteger(com.android.internal.R.integer.config_dockedStackDividerSnapMode);
          mFixedRatio = res.getFraction(
                  com.android.internal.R.fraction.docked_stack_divider_fixed_ratio, 1, 1);
          mMinimalSizeResizableTask = res.getDimensionPixelSize(
                  com.android.internal.R.dimen.default_minimal_size_resizable_task);
          mTaskHeightInMinimizedMode = res.getDimensionPixelSize(
                  com.android.internal.R.dimen.task_height_of_minimized_mode);
          calculateTargets(isHorizontalDivision, dockSide);
          mFirstSplitTarget = mTargets.get(1);
          mLastSplitTarget = mTargets.get(mTargets.size() - 2);
          mDismissStartTarget = mTargets.get(0);
          mDismissEndTarget = mTargets.get(mTargets.size() - 1);
          mMiddleTarget = mTargets.get(mTargets.size() / 2);
          mMiddleTarget.isMiddleTarget = true;
      }
  
      /**
       * @return whether it's feasible to enable split screen in the current configuration, i.e. when
       *         snapping in the middle both tasks are larger than the minimal task size.
       */
      public boolean isSplitScreenFeasible() {
          int statusBarSize = mInsets.top;
          int navBarSize = mIsHorizontalDivision ? mInsets.bottom : mInsets.right;
          int size = mIsHorizontalDivision
                  ? mDisplayHeight
                  : mDisplayWidth;
          int availableSpace = size - navBarSize - statusBarSize - mDividerSize;
          return availableSpace / 2 >= mMinimalSizeResizableTask;
      }
  
      public SnapTarget calculateSnapTarget(int position, float velocity) {
          return calculateSnapTarget(position, velocity, true /* hardDismiss */);
      }
  
      /**
       * @param position the top/left position of the divider
       * @param velocity current dragging velocity
       * @param hardDismiss if set, make it a bit harder to get reach the dismiss targets
       */
      public SnapTarget calculateSnapTarget(int position, float velocity, boolean hardDismiss) {
          if (position < mFirstSplitTarget.position && velocity < -mMinDismissVelocityPxPerSecond) {
              return mDismissStartTarget;
          }
          if (position > mLastSplitTarget.position && velocity > mMinDismissVelocityPxPerSecond) {
              return mDismissEndTarget;
          }
          if (Math.abs(velocity) < mMinFlingVelocityPxPerSecond) {
              return snap(position, hardDismiss);
          }
          if (velocity < 0) {
              return mFirstSplitTarget;
          } else {
              return mLastSplitTarget;
          }
      }
  
      public SnapTarget calculateNonDismissingSnapTarget(int position) {
          SnapTarget target = snap(position, false /* hardDismiss */);
          if (target == mDismissStartTarget) {
              return mFirstSplitTarget;
          } else if (target == mDismissEndTarget) {
              return mLastSplitTarget;
          } else {
              return target;
          }
      }
  
      public float calculateDismissingFraction(int position) {
          if (position < mFirstSplitTarget.position) {
              return 1f - (float) (position - getStartInset())
                      / (mFirstSplitTarget.position - getStartInset());
          } else if (position > mLastSplitTarget.position) {
              return (float) (position - mLastSplitTarget.position)
                      / (mDismissEndTarget.position - mLastSplitTarget.position - mDividerSize);
          }
          return 0f;
      }
  
      public SnapTarget getClosestDismissTarget(int position) {
          if (position < mFirstSplitTarget.position) {
              return mDismissStartTarget;
          } else if (position > mLastSplitTarget.position) {
              return mDismissEndTarget;
          } else if (position - mDismissStartTarget.position
                  < mDismissEndTarget.position - position) {
              return mDismissStartTarget;
          } else {
              return mDismissEndTarget;
          }
      }
  
      public SnapTarget getFirstSplitTarget() {
          return mFirstSplitTarget;
      }
  
      public SnapTarget getLastSplitTarget() {
          return mLastSplitTarget;
      }
  
      public SnapTarget getDismissStartTarget() {
          return mDismissStartTarget;
      }
  
      public SnapTarget getDismissEndTarget() {
          return mDismissEndTarget;
      }
  
      private int getStartInset() {
          if (mIsHorizontalDivision) {
              return mInsets.top;
          } else {
              return mInsets.left;
          }
      }
  
      private int getEndInset() {
          if (mIsHorizontalDivision) {
              return mInsets.bottom;
          } else {
              return mInsets.right;
          }
      }
  
      private SnapTarget snap(int position, boolean hardDismiss) {
          int minIndex = -1;
          float minDistance = Float.MAX_VALUE;
          int size = mTargets.size();
          for (int i = 0; i < size; i++) {
              SnapTarget target = mTargets.get(i);
              float distance = Math.abs(position - target.position);
              if (hardDismiss) {
                  distance /= target.distanceMultiplier;
              }
              if (distance < minDistance) {
                  minIndex = i;
                  minDistance = distance;
              }
          }
          return mTargets.get(minIndex);
      }
  
      private void calculateTargets(boolean isHorizontalDivision, int dockedSide) {
          mTargets.clear();
          int dividerMax = isHorizontalDivision
                  ? mDisplayHeight
                  : mDisplayWidth;
          int navBarSize = isHorizontalDivision ? mInsets.bottom : mInsets.right;
          int startPos = -mDividerSize;
          if (dockedSide == DOCKED_RIGHT) {
              startPos += mInsets.left;
          }
          mTargets.add(new SnapTarget(startPos, startPos, SnapTarget.FLAG_DISMISS_START,
                  0.35f));
          switch (mSnapMode) {
              case SNAP_MODE_16_9:
                  addRatio16_9Targets(isHorizontalDivision, dividerMax);
                  break;
              case SNAP_FIXED_RATIO:
                  addFixedDivisionTargets(isHorizontalDivision, dividerMax);
                  break;
              case SNAP_ONLY_1_1:
                  addMiddleTarget(isHorizontalDivision);
                  break;
              case SNAP_MODE_MINIMIZED:
                  addMinimizedTarget(isHorizontalDivision, dockedSide);
                  break;
          }
          mTargets.add(new SnapTarget(dividerMax - navBarSize, dividerMax,
                  SnapTarget.FLAG_DISMISS_END, 0.35f));
      }
  
      private void addNonDismissingTargets(boolean isHorizontalDivision, int topPosition,
              int bottomPosition, int dividerMax) {
          maybeAddTarget(topPosition, topPosition - mInsets.top);
          addMiddleTarget(isHorizontalDivision);
          maybeAddTarget(bottomPosition, dividerMax - mInsets.bottom
                  - (bottomPosition + mDividerSize));
      }
  
      private void addFixedDivisionTargets(boolean isHorizontalDivision, int dividerMax) {
          int start = isHorizontalDivision ? mInsets.top : mInsets.left;
          int end = isHorizontalDivision
                  ? mDisplayHeight - mInsets.bottom
                  : mDisplayWidth - mInsets.right;
          int size = (int) (mFixedRatio * (end - start)) - mDividerSize / 2;
          int topPosition = start + size;
          int bottomPosition = end - size - mDividerSize;
          addNonDismissingTargets(isHorizontalDivision, topPosition, bottomPosition, dividerMax);
      }
  
      private void addRatio16_9Targets(boolean isHorizontalDivision, int dividerMax) {
          int start = isHorizontalDivision ? mInsets.top : mInsets.left;
          int end = isHorizontalDivision
                  ? mDisplayHeight - mInsets.bottom
                  : mDisplayWidth - mInsets.right;
          int startOther = isHorizontalDivision ? mInsets.left : mInsets.top;
          int endOther = isHorizontalDivision
                  ? mDisplayWidth - mInsets.right
                  : mDisplayHeight - mInsets.bottom;
          float size = 9.0f / 16.0f * (endOther - startOther);
          int sizeInt = (int) Math.floor(size);
          int topPosition = start + sizeInt;
          int bottomPosition = end - sizeInt - mDividerSize;
          addNonDismissingTargets(isHorizontalDivision, topPosition, bottomPosition, dividerMax);
      }
  
      /**
       * Adds a target at {@param position} but only if the area with size of {@param smallerSize}
       * meets the minimal size requirement.
       */
      private void maybeAddTarget(int position, int smallerSize) {
          if (smallerSize >= mMinimalSizeResizableTask) {
              mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE));
          }
      }
  
      private void addMiddleTarget(boolean isHorizontalDivision) {
          int position = DockedDividerUtils.calculateMiddlePosition(isHorizontalDivision,
                  mInsets, mDisplayWidth, mDisplayHeight, mDividerSize);
          mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE));
      }
  
      private void addMinimizedTarget(boolean isHorizontalDivision, int dockedSide) {
          // In portrait offset the position by the statusbar height, in landscape add the statusbar
          // height as well to match portrait offset
          int position = mTaskHeightInMinimizedMode + mInsets.top;
          if (!isHorizontalDivision) {
              if (dockedSide == DOCKED_LEFT) {
                  position += mInsets.left;
              } else if (dockedSide == DOCKED_RIGHT) {
                  position = mDisplayWidth - position - mInsets.right - mDividerSize;
              }
          }
          mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE));
      }
  
      public SnapTarget getMiddleTarget() {
          return mMiddleTarget;
      }
  
      public SnapTarget getNextTarget(SnapTarget snapTarget) {
          int index = mTargets.indexOf(snapTarget);
          if (index != -1 && index < mTargets.size() - 1) {
              return mTargets.get(index + 1);
          }
          return snapTarget;
      }
  
      public SnapTarget getPreviousTarget(SnapTarget snapTarget) {
          int index = mTargets.indexOf(snapTarget);
          if (index != -1 && index > 0) {
              return mTargets.get(index - 1);
          }
          return snapTarget;
      }
  
      /**
       * @return whether or not there are more than 1 split targets that do not include the two
       * dismiss targets, used in deciding to display the middle target for accessibility
       */
      public boolean showMiddleSplitTargetForAccessibility() {
          return (mTargets.size() - 2) > 1;
      }
  
      public boolean isFirstSplitTargetAvailable() {
          return mFirstSplitTarget != mMiddleTarget;
      }
  
      public boolean isLastSplitTargetAvailable() {
          return mLastSplitTarget != mMiddleTarget;
      }
  
      /**
       * Cycles through all non-dismiss targets with a stepping of {@param increment}. It moves left
       * if {@param increment} is negative and moves right otherwise.
       */
      public SnapTarget cycleNonDismissTarget(SnapTarget snapTarget, int increment) {
          int index = mTargets.indexOf(snapTarget);
          if (index != -1) {
              SnapTarget newTarget = mTargets.get((index + mTargets.size() + increment)
                      % mTargets.size());
              if (newTarget == mDismissStartTarget) {
                  return mLastSplitTarget;
              } else if (newTarget == mDismissEndTarget) {
                  return mFirstSplitTarget;
              } else {
                  return newTarget;
              }
          }
          return snapTarget;
      }
  
      /**
       * Represents a snap target for the divider.
       */
      public static class SnapTarget {
          public static final int FLAG_NONE = 0;
  
          /** If the divider reaches this value, the left/top task should be dismissed. */
          public static final int FLAG_DISMISS_START = 1;
  
          /** If the divider reaches this value, the right/bottom task should be dismissed */
          public static final int FLAG_DISMISS_END = 2;
  
          /** Position of this snap target. The right/bottom edge of the top/left task snaps here. */
          public final int position;
  
          /**
           * Like {@link #position}, but used to calculate the task bounds which might be different
           * from the stack bounds.
           */
          public final int taskPosition;
  
          public final int flag;
  
          public boolean isMiddleTarget;
  
          /**
           * Multiplier used to calculate distance to snap position. The lower this value, the harder
           * it's to snap on this target
           */
          private final float distanceMultiplier;
  
          public SnapTarget(int position, int taskPosition, int flag) {
              this(position, taskPosition, flag, 1f);
          }
  
          public SnapTarget(int position, int taskPosition, int flag, float distanceMultiplier) {
              this.position = position;
              this.taskPosition = taskPosition;
              this.flag = flag;
              this.distanceMultiplier = distanceMultiplier;
          }
      }
  }
  