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 
31 import com.android.window.flags.Flags;
32 import com.android.wm.shell.R;
33 import com.android.wm.shell.common.DisplayController;
34 import com.android.wm.shell.shared.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 
84         repositionTaskBounds.set(taskBoundsAtDragStart);
85 
86         // Make sure the new resizing destination in any direction falls within the stable bounds.
87         // If not, set the bounds back to the old location that was valid to avoid conflicts with
88         // some regions such as the gesture area.
89         if ((ctrlType & CTRL_TYPE_LEFT) != 0) {
90             final int candidateLeft = repositionTaskBounds.left + (int) delta.x;
91             repositionTaskBounds.left = (candidateLeft > stableBounds.left)
92                     ? candidateLeft : oldLeft;
93         }
94         if ((ctrlType & CTRL_TYPE_RIGHT) != 0) {
95             final int candidateRight = repositionTaskBounds.right + (int) delta.x;
96             repositionTaskBounds.right = (candidateRight < stableBounds.right)
97                     ? candidateRight : oldRight;
98         }
99         if ((ctrlType & CTRL_TYPE_TOP) != 0) {
100             final int candidateTop = repositionTaskBounds.top + (int) delta.y;
101             repositionTaskBounds.top = (candidateTop > stableBounds.top)
102                     ? candidateTop : oldTop;
103         }
104         if ((ctrlType & CTRL_TYPE_BOTTOM) != 0) {
105             final int candidateBottom = repositionTaskBounds.bottom + (int) delta.y;
106             repositionTaskBounds.bottom = (candidateBottom < stableBounds.bottom)
107                     ? candidateBottom : oldBottom;
108         }
109         // If width or height are negative or less than the minimum width or height, revert the
110         // respective bounds to use previous bound dimensions.
111         if (repositionTaskBounds.width() < getMinWidth(displayController, windowDecoration)) {
112             repositionTaskBounds.right = oldRight;
113             repositionTaskBounds.left = oldLeft;
114         }
115         if (repositionTaskBounds.height() < getMinHeight(displayController, windowDecoration)) {
116             repositionTaskBounds.top = oldTop;
117             repositionTaskBounds.bottom = oldBottom;
118         }
119         // If there are no changes to the bounds after checking new bounds against minimum width
120         // and height, do not set bounds and return false
121         if (oldLeft == repositionTaskBounds.left && oldTop == repositionTaskBounds.top
122                 && oldRight == repositionTaskBounds.right
123                 && oldBottom == repositionTaskBounds.bottom) {
124             return false;
125         }
126         return true;
127     }
128 
129     /**
130      * Set bounds using a {@link SurfaceControl.Transaction}.
131      */
setPositionOnDrag(WindowDecoration decoration, Rect repositionTaskBounds, Rect taskBoundsAtDragStart, PointF repositionStartPoint, SurfaceControl.Transaction t, float x, float y)132     static void setPositionOnDrag(WindowDecoration decoration, Rect repositionTaskBounds,
133             Rect taskBoundsAtDragStart, PointF repositionStartPoint, SurfaceControl.Transaction t,
134             float x, float y) {
135         updateTaskBounds(repositionTaskBounds, taskBoundsAtDragStart, repositionStartPoint, x, y);
136         t.setPosition(decoration.mTaskSurface, repositionTaskBounds.left, repositionTaskBounds.top);
137     }
138 
updateTaskBounds(Rect repositionTaskBounds, Rect taskBoundsAtDragStart, PointF repositionStartPoint, float x, float y)139     static void updateTaskBounds(Rect repositionTaskBounds, Rect taskBoundsAtDragStart,
140             PointF repositionStartPoint, float x, float y) {
141         final float deltaX = x - repositionStartPoint.x;
142         final float deltaY = y - repositionStartPoint.y;
143         repositionTaskBounds.set(taskBoundsAtDragStart);
144         repositionTaskBounds.offset((int) deltaX, (int) deltaY);
145     }
146 
147     /**
148      * If task bounds are outside of provided drag area, snap the bounds to be just inside the
149      * drag area.
150      *
151      * @param repositionTaskBounds bounds determined by task positioner
152      * @param validDragArea        the area that task must be positioned inside
153      * @return whether bounds were modified
154      */
snapTaskBoundsIfNecessary(Rect repositionTaskBounds, Rect validDragArea)155     public static boolean snapTaskBoundsIfNecessary(Rect repositionTaskBounds, Rect validDragArea) {
156         // If we were never supplied a valid drag area, do not restrict movement.
157         // Otherwise, we restrict deltas to keep task position inside the Rect.
158         if (validDragArea.width() == 0) return false;
159         boolean result = false;
160         if (repositionTaskBounds.left < validDragArea.left) {
161             repositionTaskBounds.offset(validDragArea.left - repositionTaskBounds.left, 0);
162             result = true;
163         } else if (repositionTaskBounds.left > validDragArea.right) {
164             repositionTaskBounds.offset(validDragArea.right - repositionTaskBounds.left, 0);
165             result = true;
166         }
167         if (repositionTaskBounds.top < validDragArea.top) {
168             repositionTaskBounds.offset(0, validDragArea.top - repositionTaskBounds.top);
169             result = true;
170         } else if (repositionTaskBounds.top > validDragArea.bottom) {
171             repositionTaskBounds.offset(0, validDragArea.bottom - repositionTaskBounds.top);
172             result = true;
173         }
174         return result;
175     }
176 
getMinWidth(DisplayController displayController, WindowDecoration windowDecoration)177     private static float getMinWidth(DisplayController displayController,
178             WindowDecoration windowDecoration) {
179         return windowDecoration.mTaskInfo.minWidth < 0 ? getDefaultMinWidth(displayController,
180                 windowDecoration)
181                 : windowDecoration.mTaskInfo.minWidth;
182     }
183 
getMinHeight(DisplayController displayController, WindowDecoration windowDecoration)184     private static float getMinHeight(DisplayController displayController,
185             WindowDecoration windowDecoration) {
186         return windowDecoration.mTaskInfo.minHeight < 0 ? getDefaultMinHeight(displayController,
187                 windowDecoration)
188                 : windowDecoration.mTaskInfo.minHeight;
189     }
190 
getDefaultMinWidth(DisplayController displayController, WindowDecoration windowDecoration)191     private static float getDefaultMinWidth(DisplayController displayController,
192             WindowDecoration windowDecoration) {
193         if (isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext)) {
194             return WindowDecoration.loadDimensionPixelSize(
195                     windowDecoration.mDecorWindowContext.getResources(),
196                     R.dimen.desktop_mode_minimum_window_width);
197         }
198         return getDefaultMinSize(displayController, windowDecoration);
199     }
200 
getDefaultMinHeight(DisplayController displayController, WindowDecoration windowDecoration)201     private static float getDefaultMinHeight(DisplayController displayController,
202             WindowDecoration windowDecoration) {
203         if (isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext)) {
204             return WindowDecoration.loadDimensionPixelSize(
205                     windowDecoration.mDecorWindowContext.getResources(),
206                     R.dimen.desktop_mode_minimum_window_height);
207         }
208         return getDefaultMinSize(displayController, windowDecoration);
209     }
210 
getDefaultMinSize(DisplayController displayController, WindowDecoration windowDecoration)211     private static float getDefaultMinSize(DisplayController displayController,
212             WindowDecoration windowDecoration) {
213         float density =  displayController.getDisplayLayout(windowDecoration.mTaskInfo.displayId)
214                 .densityDpi() * DisplayMetrics.DENSITY_DEFAULT_SCALE;
215         return windowDecoration.mTaskInfo.defaultMinSize * density;
216     }
217 
isSizeConstraintForDesktopModeEnabled(Context context)218     private static boolean isSizeConstraintForDesktopModeEnabled(Context context) {
219         return DesktopModeStatus.canEnterDesktopMode(context)
220                 && Flags.enableDesktopWindowingSizeConstraints();
221     }
222 
223     interface DragStartListener {
224         /**
225          * Inform the implementing class that a drag resize has started
226          *
227          * @param taskId id of this positioner's {@link WindowDecoration}
228          */
onDragStart(int taskId)229         void onDragStart(int taskId);
230     }
231 }
232