1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.wm.shell.windowdecor; 18 19 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM; 20 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT; 21 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT; 22 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP; 23 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED; 24 25 import android.content.Context; 26 import android.graphics.PointF; 27 import android.graphics.Rect; 28 import android.util.DisplayMetrics; 29 import android.view.SurfaceControl; 30 import android.window.DesktopModeFlags; 31 32 import com.android.wm.shell.R; 33 import com.android.wm.shell.common.DisplayController; 34 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; 35 36 /** 37 * Utility class that contains logic common to classes implementing {@link DragPositioningCallback} 38 * Specifically, this class contains logic for determining changed bounds from a drag input 39 * and applying that change to the task bounds when applicable. 40 */ 41 public class DragPositioningCallbackUtility { 42 /** 43 * Determine the delta between input's current point and the input start point. 44 * 45 * @param inputX current input x coordinate 46 * @param inputY current input y coordinate 47 * @param repositionStartPoint initial input coordinate 48 * @return delta between these two points 49 */ calculateDelta(float inputX, float inputY, PointF repositionStartPoint)50 static PointF calculateDelta(float inputX, float inputY, PointF repositionStartPoint) { 51 final float deltaX = inputX - repositionStartPoint.x; 52 final float deltaY = inputY - repositionStartPoint.y; 53 return new PointF(deltaX, deltaY); 54 } 55 56 /** 57 * Based on type of resize and delta provided, calculate the new bounds to display for this 58 * task. 59 * 60 * @param ctrlType type of drag being performed 61 * @param repositionTaskBounds the bounds the task is being repositioned to 62 * @param taskBoundsAtDragStart the bounds of the task on the first drag input event 63 * @param stableBounds bounds that represent the resize limit of this task 64 * @param delta difference between start input and current input in x/y 65 * coordinates 66 * @param windowDecoration window decoration of the task being dragged 67 * @return whether this method changed repositionTaskBounds 68 */ changeBounds(int ctrlType, Rect repositionTaskBounds, Rect taskBoundsAtDragStart, Rect stableBounds, PointF delta, DisplayController displayController, WindowDecoration windowDecoration)69 static boolean changeBounds(int ctrlType, Rect repositionTaskBounds, Rect taskBoundsAtDragStart, 70 Rect stableBounds, PointF delta, DisplayController displayController, 71 WindowDecoration windowDecoration) { 72 // If task is being dragged rather than resized, return since this method only handles 73 // with resizing 74 if (ctrlType == CTRL_TYPE_UNDEFINED) { 75 return false; 76 } 77 78 final int oldLeft = repositionTaskBounds.left; 79 final int oldTop = repositionTaskBounds.top; 80 final int oldRight = repositionTaskBounds.right; 81 final int oldBottom = repositionTaskBounds.bottom; 82 83 repositionTaskBounds.set(taskBoundsAtDragStart); 84 85 boolean isAspectRatioMaintained = true; 86 // Make sure the new resizing destination in any direction falls within the stable bounds. 87 if ((ctrlType & CTRL_TYPE_LEFT) != 0) { 88 repositionTaskBounds.left = Math.max(repositionTaskBounds.left + (int) delta.x, 89 stableBounds.left); 90 if (repositionTaskBounds.left == stableBounds.left 91 && repositionTaskBounds.left + (int) delta.x != stableBounds.left) { 92 // If the task edge have been set to the stable bounds and not due to the users 93 // drag, the aspect ratio of the task will not be maintained. 94 isAspectRatioMaintained = false; 95 } 96 } 97 if ((ctrlType & CTRL_TYPE_RIGHT) != 0) { 98 repositionTaskBounds.right = Math.min(repositionTaskBounds.right + (int) delta.x, 99 stableBounds.right); 100 if (repositionTaskBounds.right == stableBounds.right 101 && repositionTaskBounds.right + (int) delta.x != stableBounds.right) { 102 // If the task edge have been set to the stable bounds and not due to the users 103 // drag, the aspect ratio of the task will not be maintained. 104 isAspectRatioMaintained = false; 105 } 106 } 107 if ((ctrlType & CTRL_TYPE_TOP) != 0) { 108 repositionTaskBounds.top = Math.max(repositionTaskBounds.top + (int) delta.y, 109 stableBounds.top); 110 if (repositionTaskBounds.top == stableBounds.top 111 && repositionTaskBounds.top + (int) delta.y != stableBounds.top) { 112 // If the task edge have been set to the stable bounds and not due to the users 113 // drag, the aspect ratio of the task will not be maintained. 114 isAspectRatioMaintained = false; 115 } 116 } 117 if ((ctrlType & CTRL_TYPE_BOTTOM) != 0) { 118 repositionTaskBounds.bottom = Math.min(repositionTaskBounds.bottom + (int) delta.y, 119 stableBounds.bottom); 120 if (repositionTaskBounds.bottom == stableBounds.bottom 121 && repositionTaskBounds.bottom + (int) delta.y != stableBounds.bottom) { 122 // If the task edge have been set to the stable bounds and not due to the users 123 // drag, the aspect ratio of the task will not be maintained. 124 isAspectRatioMaintained = false; 125 } 126 } 127 128 // If width or height are negative or exceeding the width or height constraints, revert the 129 // respective bounds to use previous bound dimensions. 130 if (isExceedingWidthConstraint(repositionTaskBounds.width(), 131 /* startingWidth= */ oldRight - oldLeft, stableBounds, displayController, 132 windowDecoration)) { 133 repositionTaskBounds.right = oldRight; 134 repositionTaskBounds.left = oldLeft; 135 isAspectRatioMaintained = false; 136 } 137 if (isExceedingHeightConstraint(repositionTaskBounds.height(), 138 /* startingHeight= */oldBottom - oldTop, stableBounds, displayController, 139 windowDecoration)) { 140 repositionTaskBounds.top = oldTop; 141 repositionTaskBounds.bottom = oldBottom; 142 isAspectRatioMaintained = false; 143 } 144 145 // If the application is unresizeable and any bounds have been set back to their old 146 // location or to a stable bound edge, reset all the bounds to maintain the applications 147 // aspect ratio. 148 if (DesktopModeFlags.ENABLE_WINDOWING_SCALED_RESIZING.isTrue() 149 && !isAspectRatioMaintained && !windowDecoration.mTaskInfo.isResizeable) { 150 repositionTaskBounds.top = oldTop; 151 repositionTaskBounds.bottom = oldBottom; 152 repositionTaskBounds.right = oldRight; 153 repositionTaskBounds.left = oldLeft; 154 } 155 156 // If there are no changes to the bounds after checking new bounds against minimum and 157 // maximum width and height, do not set bounds and return false 158 return oldLeft != repositionTaskBounds.left || oldTop != repositionTaskBounds.top 159 || oldRight != repositionTaskBounds.right 160 || oldBottom != repositionTaskBounds.bottom; 161 } 162 163 /** 164 * Set bounds using a {@link SurfaceControl.Transaction}. 165 */ setPositionOnDrag(WindowDecoration decoration, Rect repositionTaskBounds, Rect taskBoundsAtDragStart, PointF repositionStartPoint, SurfaceControl.Transaction t, float x, float y)166 static void setPositionOnDrag(WindowDecoration decoration, Rect repositionTaskBounds, 167 Rect taskBoundsAtDragStart, PointF repositionStartPoint, SurfaceControl.Transaction t, 168 float x, float y) { 169 updateTaskBounds(repositionTaskBounds, taskBoundsAtDragStart, repositionStartPoint, x, y); 170 t.setPosition(decoration.mTaskSurface, repositionTaskBounds.left, repositionTaskBounds.top); 171 } 172 updateTaskBounds(Rect repositionTaskBounds, Rect taskBoundsAtDragStart, PointF repositionStartPoint, float x, float y)173 static void updateTaskBounds(Rect repositionTaskBounds, Rect taskBoundsAtDragStart, 174 PointF repositionStartPoint, float x, float y) { 175 final float deltaX = x - repositionStartPoint.x; 176 final float deltaY = y - repositionStartPoint.y; 177 repositionTaskBounds.set(taskBoundsAtDragStart); 178 repositionTaskBounds.offset((int) deltaX, (int) deltaY); 179 } 180 181 /** 182 * If task bounds are outside of provided drag area, snap the bounds to be just inside the 183 * drag area. 184 * 185 * @param repositionTaskBounds bounds determined by task positioner 186 * @param validDragArea the area that task must be positioned inside 187 * @return whether bounds were modified 188 */ snapTaskBoundsIfNecessary(Rect repositionTaskBounds, Rect validDragArea)189 public static boolean snapTaskBoundsIfNecessary(Rect repositionTaskBounds, Rect validDragArea) { 190 // If we were never supplied a valid drag area, do not restrict movement. 191 // Otherwise, we restrict deltas to keep task position inside the Rect. 192 if (validDragArea.width() == 0) return false; 193 boolean result = false; 194 if (repositionTaskBounds.left < validDragArea.left) { 195 repositionTaskBounds.offset(validDragArea.left - repositionTaskBounds.left, 0); 196 result = true; 197 } else if (repositionTaskBounds.left > validDragArea.right) { 198 repositionTaskBounds.offset(validDragArea.right - repositionTaskBounds.left, 0); 199 result = true; 200 } 201 if (repositionTaskBounds.top < validDragArea.top) { 202 repositionTaskBounds.offset(0, validDragArea.top - repositionTaskBounds.top); 203 result = true; 204 } else if (repositionTaskBounds.top > validDragArea.bottom) { 205 repositionTaskBounds.offset(0, validDragArea.bottom - repositionTaskBounds.top); 206 result = true; 207 } 208 return result; 209 } 210 211 /** 212 * Checks whether the new task bounds exceed the allowed width. 213 * 214 * @param repositionedWidth task width after repositioning. 215 * @param startingWidth task width before repositioning. 216 * @param maxResizeBounds stable bounds for display. 217 * @param displayController display controller for the task being checked. 218 * @param windowDecoration contains decor info and helpers for the task. 219 * @return whether the task is exceeding any of the width constrains, minimum or maximum. 220 */ isExceedingWidthConstraint(int repositionedWidth, int startingWidth, Rect maxResizeBounds, DisplayController displayController, WindowDecoration windowDecoration)221 public static boolean isExceedingWidthConstraint(int repositionedWidth, int startingWidth, 222 Rect maxResizeBounds, DisplayController displayController, 223 WindowDecoration windowDecoration) { 224 boolean isSizeIncreasing = (repositionedWidth - startingWidth) > 0; 225 // Check if width is less than the minimum width constraint. 226 if (repositionedWidth < getMinWidth(displayController, windowDecoration)) { 227 // Only allow width to be increased if it is already below minimum. 228 return !isSizeIncreasing; 229 } 230 // Check if width is more than the maximum resize bounds on desktop windowing mode. 231 // Only allow width to be decreased if it already exceeds maximum. 232 return isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext) 233 && repositionedWidth > maxResizeBounds.width() && isSizeIncreasing; 234 } 235 236 /** 237 * Checks whether the new task bounds exceed the allowed height. 238 * 239 * @param repositionedHeight task's height after repositioning. 240 * @param startingHeight task's height before repositioning. 241 * @param maxResizeBounds stable bounds for display. 242 * @param displayController display controller for the task being checked. 243 * @param windowDecoration contains decor info and helpers for the task. 244 * @return whether the task is exceeding any of the height constrains, minimum or maximum. 245 */ isExceedingHeightConstraint(int repositionedHeight, int startingHeight, Rect maxResizeBounds, DisplayController displayController, WindowDecoration windowDecoration)246 public static boolean isExceedingHeightConstraint(int repositionedHeight, int startingHeight, 247 Rect maxResizeBounds, DisplayController displayController, 248 WindowDecoration windowDecoration) { 249 boolean isSizeIncreasing = (repositionedHeight - startingHeight) > 0; 250 // Check if height is less than the minimum height constraint. 251 if (repositionedHeight < getMinHeight(displayController, windowDecoration)) { 252 // Only allow height to be increased if it is already below minimum. 253 return !isSizeIncreasing; 254 } 255 // Check if height is more than the maximum resize bounds on desktop windowing mode. 256 // Only allow height to be decreased if it already exceeds maximum. 257 return isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext) 258 && repositionedHeight > maxResizeBounds.height() && isSizeIncreasing; 259 } 260 getMinWidth(DisplayController displayController, WindowDecoration windowDecoration)261 private static float getMinWidth(DisplayController displayController, 262 WindowDecoration windowDecoration) { 263 return windowDecoration.mTaskInfo.minWidth < 0 ? getDefaultMinWidth(displayController, 264 windowDecoration) 265 : windowDecoration.mTaskInfo.minWidth; 266 } 267 getMinHeight(DisplayController displayController, WindowDecoration windowDecoration)268 private static float getMinHeight(DisplayController displayController, 269 WindowDecoration windowDecoration) { 270 return windowDecoration.mTaskInfo.minHeight < 0 ? getDefaultMinHeight(displayController, 271 windowDecoration) 272 : windowDecoration.mTaskInfo.minHeight; 273 } 274 getDefaultMinWidth(DisplayController displayController, WindowDecoration windowDecoration)275 private static float getDefaultMinWidth(DisplayController displayController, 276 WindowDecoration windowDecoration) { 277 if (isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext)) { 278 return WindowDecoration.loadDimensionPixelSize( 279 windowDecoration.mDecorWindowContext.getResources(), 280 R.dimen.desktop_mode_minimum_window_width); 281 } 282 return getDefaultMinSize(displayController, windowDecoration); 283 } 284 getDefaultMinHeight(DisplayController displayController, WindowDecoration windowDecoration)285 private static float getDefaultMinHeight(DisplayController displayController, 286 WindowDecoration windowDecoration) { 287 if (isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext)) { 288 return WindowDecoration.loadDimensionPixelSize( 289 windowDecoration.mDecorWindowContext.getResources(), 290 R.dimen.desktop_mode_minimum_window_height); 291 } 292 return getDefaultMinSize(displayController, windowDecoration); 293 } 294 getDefaultMinSize(DisplayController displayController, WindowDecoration windowDecoration)295 private static float getDefaultMinSize(DisplayController displayController, 296 WindowDecoration windowDecoration) { 297 float density = displayController.getDisplayLayout(windowDecoration.mTaskInfo.displayId) 298 .densityDpi() * DisplayMetrics.DENSITY_DEFAULT_SCALE; 299 return windowDecoration.mTaskInfo.defaultMinSize * density; 300 } 301 isSizeConstraintForDesktopModeEnabled(Context context)302 private static boolean isSizeConstraintForDesktopModeEnabled(Context context) { 303 return DesktopModeStatus.canEnterDesktopMode(context) 304 && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS.isTrue(); 305 } 306 307 public interface DragEventListener { 308 /** 309 * Inform the implementing class that a drag resize has started 310 * 311 * @param taskId id of this positioner's {@link WindowDecoration} 312 */ onDragStart(int taskId)313 void onDragStart(int taskId); 314 315 /** 316 * Inform the implementing class that a drag move has started. 317 * 318 * @param taskId id of this positioner's {@link WindowDecoration} 319 */ onDragMove(int taskId)320 void onDragMove(int taskId); 321 } 322 } 323