• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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 android.view.InputDevice.SOURCE_TOUCHSCREEN;
20 
21 import static com.android.window.flags.Flags.enableWindowingEdgeDragResize;
22 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM;
23 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT;
24 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT;
25 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP;
26 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED;
27 
28 import android.annotation.NonNull;
29 import android.content.res.Resources;
30 import android.graphics.Point;
31 import android.graphics.Rect;
32 import android.graphics.Region;
33 import android.util.Size;
34 import android.view.MotionEvent;
35 
36 import androidx.annotation.Nullable;
37 import androidx.annotation.VisibleForTesting;
38 
39 import com.android.wm.shell.R;
40 
41 import java.util.Objects;
42 
43 /**
44  * Geometry for a drag resize region for a particular window.
45  */
46 final class DragResizeWindowGeometry {
47     // TODO(b/337264971) clean up when no longer needed
48     @VisibleForTesting static final boolean DEBUG = true;
49     // The additional width to apply to edge resize bounds just for logging when a touch is
50     // close.
51     @VisibleForTesting static final int EDGE_DEBUG_BUFFER = 15;
52     private final int mTaskCornerRadius;
53     private final Size mTaskSize;
54     // The size of the handle applied to the edges of the window, for the user to drag resize.
55     private final int mResizeHandleThickness;
56     // The task corners to permit drag resizing with a course input, such as touch.
57 
58     private final @NonNull TaskCorners mLargeTaskCorners;
59     // The task corners to permit drag resizing with a fine input, such as stylus or cursor.
60     private final @NonNull TaskCorners mFineTaskCorners;
61     // The bounds for each edge drag region, which can resize the task in one direction.
62     private final @NonNull TaskEdges mTaskEdges;
63     // Extra-large edge bounds for logging to help debug when an edge resize is ignored.
64     private final @Nullable TaskEdges mDebugTaskEdges;
65 
DragResizeWindowGeometry(int taskCornerRadius, @NonNull Size taskSize, int resizeHandleThickness, int fineCornerSize, int largeCornerSize)66     DragResizeWindowGeometry(int taskCornerRadius, @NonNull Size taskSize,
67             int resizeHandleThickness, int fineCornerSize, int largeCornerSize) {
68         mTaskCornerRadius = taskCornerRadius;
69         mTaskSize = taskSize;
70         mResizeHandleThickness = resizeHandleThickness;
71 
72         mLargeTaskCorners = new TaskCorners(mTaskSize, largeCornerSize);
73         mFineTaskCorners = new TaskCorners(mTaskSize, fineCornerSize);
74 
75         // Save touch areas for each edge.
76         mTaskEdges = new TaskEdges(mTaskSize, mResizeHandleThickness);
77         if (DEBUG) {
78             mDebugTaskEdges = new TaskEdges(mTaskSize, mResizeHandleThickness + EDGE_DEBUG_BUFFER);
79         } else {
80             mDebugTaskEdges = null;
81         }
82     }
83 
84     /**
85      * Returns the resource value to use for the resize handle on the edge of the window.
86      */
getResizeEdgeHandleSize(@onNull Resources res)87     static int getResizeEdgeHandleSize(@NonNull Resources res) {
88         return enableWindowingEdgeDragResize()
89                 ? res.getDimensionPixelSize(R.dimen.desktop_mode_edge_handle)
90                 : res.getDimensionPixelSize(R.dimen.freeform_resize_handle);
91     }
92 
93     /**
94      * Returns the resource value to use for course input, such as touch, that benefits from a large
95      * square on each of the window's corners.
96      */
getLargeResizeCornerSize(@onNull Resources res)97     static int getLargeResizeCornerSize(@NonNull Resources res) {
98         return res.getDimensionPixelSize(R.dimen.desktop_mode_corner_resize_large);
99     }
100 
101     /**
102      * Returns the resource value to use for fine input, such as stylus, that can use a smaller
103      * square on each of the window's corners.
104      */
getFineResizeCornerSize(@onNull Resources res)105     static int getFineResizeCornerSize(@NonNull Resources res) {
106         return res.getDimensionPixelSize(R.dimen.freeform_resize_corner);
107     }
108 
109     /**
110      * Returns the size of the task this geometry is calculated for.
111      */
getTaskSize()112     @NonNull Size getTaskSize() {
113         // Safe to return directly since size is immutable.
114         return mTaskSize;
115     }
116 
117     /**
118      * Returns the union of all regions that can be touched for drag resizing; the corners window
119      * and window edges.
120      */
union(@onNull Region region)121     void union(@NonNull Region region) {
122         // Apply the edge resize regions.
123         if (inDebugMode()) {
124             // Use the larger edge sizes if we are debugging, to be able to log if we ignored a
125             // touch due to the size of the edge region.
126             mDebugTaskEdges.union(region);
127         } else {
128             mTaskEdges.union(region);
129         }
130 
131         if (enableWindowingEdgeDragResize()) {
132             // Apply the corners as well for the larger corners, to ensure we capture all possible
133             // touches.
134             mLargeTaskCorners.union(region);
135         } else {
136             // Only apply fine corners for the legacy approach.
137             mFineTaskCorners.union(region);
138         }
139     }
140 
141     /**
142      * Returns if this MotionEvent should be handled, based on its source and position.
143      */
shouldHandleEvent(@onNull MotionEvent e, @NonNull Point offset)144     boolean shouldHandleEvent(@NonNull MotionEvent e, @NonNull Point offset) {
145         final float x = e.getX(0) + offset.x;
146         final float y = e.getY(0) + offset.y;
147 
148         if (enableWindowingEdgeDragResize()) {
149             // First check if touch falls within a corner.
150             // Large corner bounds are used for course input like touch, otherwise fine bounds.
151             boolean result = isEventFromTouchscreen(e)
152                     ? isInCornerBounds(mLargeTaskCorners, x, y)
153                     : isInCornerBounds(mFineTaskCorners, x, y);
154             // Check if touch falls within the edge resize handle. Limit edge resizing to stylus and
155             // mouse input.
156             if (!result && isEdgeResizePermitted(e)) {
157                 result = isInEdgeResizeBounds(x, y);
158             }
159             return result;
160         } else {
161             // Legacy uses only fine corners for touch, and edges only for non-touch input.
162             return isEventFromTouchscreen(e)
163                     ? isInCornerBounds(mFineTaskCorners, x, y)
164                     : isInEdgeResizeBounds(x, y);
165         }
166     }
167 
isEventFromTouchscreen(@onNull MotionEvent e)168     static boolean isEventFromTouchscreen(@NonNull MotionEvent e) {
169         return (e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN;
170     }
171 
isEdgeResizePermitted(@onNull MotionEvent e)172     static boolean isEdgeResizePermitted(@NonNull MotionEvent e) {
173         if (enableWindowingEdgeDragResize()) {
174             return e.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS
175                     || e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE;
176         } else {
177             return e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE;
178         }
179     }
180 
isInCornerBounds(TaskCorners corners, float xf, float yf)181     private boolean isInCornerBounds(TaskCorners corners, float xf, float yf) {
182         return corners.calculateCornersCtrlType(xf, yf) != 0;
183     }
184 
isInEdgeResizeBounds(float x, float y)185     private boolean isInEdgeResizeBounds(float x, float y) {
186         return calculateEdgeResizeCtrlType(x, y) != 0;
187     }
188 
189     /**
190      * Returns the control type for the drag-resize, based on the touch regions and this
191      * MotionEvent's coordinates.
192      * @param isTouchscreen Controls the size of the corner resize regions; touchscreen events
193      *                      (finger & stylus) are eligible for a larger area than cursor events
194      * @param isEdgeResizePermitted Indicates if the event is eligible for falling into an edge
195      *                              resize region.
196      */
197     @DragPositioningCallback.CtrlType
calculateCtrlType(boolean isTouchscreen, boolean isEdgeResizePermitted, float x, float y)198     int calculateCtrlType(boolean isTouchscreen, boolean isEdgeResizePermitted, float x, float y) {
199         if (enableWindowingEdgeDragResize()) {
200             // First check if touch falls within a corner.
201             // Large corner bounds are used for course input like touch, otherwise fine bounds.
202             int ctrlType = isTouchscreen
203                     ? mLargeTaskCorners.calculateCornersCtrlType(x, y)
204                     : mFineTaskCorners.calculateCornersCtrlType(x, y);
205 
206             // Check if touch falls within the edge resize handle, since edge resizing can apply
207             // for any input source.
208             if (ctrlType == CTRL_TYPE_UNDEFINED && isEdgeResizePermitted) {
209                 ctrlType = calculateEdgeResizeCtrlType(x, y);
210             }
211             return ctrlType;
212         } else {
213             // Legacy uses only fine corners for touch, and edges only for non-touch input.
214             return isTouchscreen
215                     ? mFineTaskCorners.calculateCornersCtrlType(x, y)
216                     : calculateEdgeResizeCtrlType(x, y);
217         }
218     }
219 
220     @DragPositioningCallback.CtrlType
calculateEdgeResizeCtrlType(float x, float y)221     private int calculateEdgeResizeCtrlType(float x, float y) {
222         if (inDebugMode() && (mDebugTaskEdges.contains((int) x, (int) y)
223                     && !mTaskEdges.contains((int) x, (int) y))) {
224             return CTRL_TYPE_UNDEFINED;
225         }
226         int ctrlType = CTRL_TYPE_UNDEFINED;
227         // mTaskCornerRadius is only used in comparing with corner regions. Comparisons with
228         // sides will use the bounds specified in setGeometry and not go into task bounds.
229         if (x < mTaskCornerRadius) {
230             ctrlType |= CTRL_TYPE_LEFT;
231         }
232         if (x > mTaskSize.getWidth() - mTaskCornerRadius) {
233             ctrlType |= CTRL_TYPE_RIGHT;
234         }
235         if (y < mTaskCornerRadius) {
236             ctrlType |= CTRL_TYPE_TOP;
237         }
238         if (y > mTaskSize.getHeight() - mTaskCornerRadius) {
239             ctrlType |= CTRL_TYPE_BOTTOM;
240         }
241         // If the touch is within one of the four corners, check if it is within the bounds of the
242         // // handle.
243         if ((ctrlType & (CTRL_TYPE_LEFT | CTRL_TYPE_RIGHT)) != 0
244                 && (ctrlType & (CTRL_TYPE_TOP | CTRL_TYPE_BOTTOM)) != 0) {
245             return checkDistanceFromCenter(ctrlType, x, y);
246         }
247         // Otherwise, we should make sure we don't resize tasks inside task bounds.
248         return (x < 0 || y < 0 || x >= mTaskSize.getWidth() || y >= mTaskSize.getHeight())
249                 ? ctrlType : CTRL_TYPE_UNDEFINED;
250     }
251 
252     /**
253      * Return {@code ctrlType} if the corner input is outside the (potentially rounded) corner of
254      * the task, and within the thickness of the resize handle. Otherwise, return 0.
255      */
256     @DragPositioningCallback.CtrlType
checkDistanceFromCenter(@ragPositioningCallback.CtrlType int ctrlType, float x, float y)257     private int checkDistanceFromCenter(@DragPositioningCallback.CtrlType int ctrlType, float x,
258             float y) {
259         final Point cornerRadiusCenter = calculateCenterForCornerRadius(ctrlType);
260         double distanceFromCenter = Math.hypot(x - cornerRadiusCenter.x, y - cornerRadiusCenter.y);
261 
262         if (distanceFromCenter < mTaskCornerRadius + mResizeHandleThickness
263                 && distanceFromCenter >= mTaskCornerRadius) {
264             return ctrlType;
265         }
266         return CTRL_TYPE_UNDEFINED;
267     }
268 
269     /**
270      * Returns center of rounded corner circle; this is simply the corner if radius is 0.
271      */
calculateCenterForCornerRadius(@ragPositioningCallback.CtrlType int ctrlType)272     private Point calculateCenterForCornerRadius(@DragPositioningCallback.CtrlType int ctrlType) {
273         int centerX;
274         int centerY;
275 
276         switch (ctrlType) {
277             case CTRL_TYPE_LEFT | CTRL_TYPE_TOP: {
278                 centerX = mTaskCornerRadius;
279                 centerY = mTaskCornerRadius;
280                 break;
281             }
282             case CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM: {
283                 centerX = mTaskCornerRadius;
284                 centerY = mTaskSize.getHeight() - mTaskCornerRadius;
285                 break;
286             }
287             case CTRL_TYPE_RIGHT | CTRL_TYPE_TOP: {
288                 centerX = mTaskSize.getWidth() - mTaskCornerRadius;
289                 centerY = mTaskCornerRadius;
290                 break;
291             }
292             case CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM: {
293                 centerX = mTaskSize.getWidth() - mTaskCornerRadius;
294                 centerY = mTaskSize.getHeight() - mTaskCornerRadius;
295                 break;
296             }
297             default: {
298                 throw new IllegalArgumentException(
299                         "ctrlType should be complex, but it's 0x" + Integer.toHexString(ctrlType));
300             }
301         }
302         return new Point(centerX, centerY);
303     }
304 
305     @Override
equals(Object obj)306     public boolean equals(Object obj) {
307         if (obj == null) return false;
308         if (this == obj) return true;
309         if (!(obj instanceof DragResizeWindowGeometry other)) return false;
310 
311         return this.mTaskCornerRadius == other.mTaskCornerRadius
312                 && this.mTaskSize.equals(other.mTaskSize)
313                 && this.mResizeHandleThickness == other.mResizeHandleThickness
314                 && this.mFineTaskCorners.equals(other.mFineTaskCorners)
315                 && this.mLargeTaskCorners.equals(other.mLargeTaskCorners)
316                 && (inDebugMode()
317                         ? this.mDebugTaskEdges.equals(other.mDebugTaskEdges)
318                         : this.mTaskEdges.equals(other.mTaskEdges));
319     }
320 
321     @Override
hashCode()322     public int hashCode() {
323         return Objects.hash(
324                 mTaskCornerRadius,
325                 mTaskSize,
326                 mResizeHandleThickness,
327                 mFineTaskCorners,
328                 mLargeTaskCorners,
329                 (inDebugMode() ? mDebugTaskEdges : mTaskEdges));
330     }
331 
inDebugMode()332     private boolean inDebugMode() {
333         return DEBUG && mDebugTaskEdges != null;
334     }
335 
336     /**
337      * Representation of the drag resize regions at the corner of the window.
338      */
339     private static class TaskCorners {
340         // The size of the square applied to the corners of the window, for the user to drag
341         // resize.
342         private final int mCornerSize;
343         // The square for each corner.
344         private final @NonNull Rect mLeftTopCornerBounds;
345         private final @NonNull Rect mRightTopCornerBounds;
346         private final @NonNull Rect mLeftBottomCornerBounds;
347         private final @NonNull Rect mRightBottomCornerBounds;
348 
TaskCorners(@onNull Size taskSize, int cornerSize)349         TaskCorners(@NonNull Size taskSize, int cornerSize) {
350             mCornerSize = cornerSize;
351             final int cornerRadius = cornerSize / 2;
352             mLeftTopCornerBounds = new Rect(
353                     -cornerRadius,
354                     -cornerRadius,
355                     cornerRadius,
356                     cornerRadius);
357 
358             mRightTopCornerBounds = new Rect(
359                     taskSize.getWidth() - cornerRadius,
360                     -cornerRadius,
361                     taskSize.getWidth() + cornerRadius,
362                     cornerRadius);
363 
364             mLeftBottomCornerBounds = new Rect(
365                     -cornerRadius,
366                     taskSize.getHeight() - cornerRadius,
367                     cornerRadius,
368                     taskSize.getHeight() + cornerRadius);
369 
370             mRightBottomCornerBounds = new Rect(
371                     taskSize.getWidth() - cornerRadius,
372                     taskSize.getHeight() - cornerRadius,
373                     taskSize.getWidth() + cornerRadius,
374                     taskSize.getHeight() + cornerRadius);
375         }
376 
377         /**
378          * Updates the region to include all four corners.
379          */
union(Region region)380         void union(Region region) {
381             region.union(mLeftTopCornerBounds);
382             region.union(mRightTopCornerBounds);
383             region.union(mLeftBottomCornerBounds);
384             region.union(mRightBottomCornerBounds);
385         }
386 
387         /**
388          * Returns the control type based on the position of the {@code MotionEvent}'s coordinates.
389          */
390         @DragPositioningCallback.CtrlType
calculateCornersCtrlType(float x, float y)391         int calculateCornersCtrlType(float x, float y) {
392             int xi = (int) x;
393             int yi = (int) y;
394             if (mLeftTopCornerBounds.contains(xi, yi)) {
395                 return CTRL_TYPE_LEFT | CTRL_TYPE_TOP;
396             }
397             if (mLeftBottomCornerBounds.contains(xi, yi)) {
398                 return CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM;
399             }
400             if (mRightTopCornerBounds.contains(xi, yi)) {
401                 return CTRL_TYPE_RIGHT | CTRL_TYPE_TOP;
402             }
403             if (mRightBottomCornerBounds.contains(xi, yi)) {
404                 return CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM;
405             }
406             return 0;
407         }
408 
409         @Override
toString()410         public String toString() {
411             return "TaskCorners of size " + mCornerSize + " for the"
412                     + " top left " + mLeftTopCornerBounds
413                     + " top right " + mRightTopCornerBounds
414                     + " bottom left " + mLeftBottomCornerBounds
415                     + " bottom right " + mRightBottomCornerBounds;
416         }
417 
418         @Override
equals(Object obj)419         public boolean equals(Object obj) {
420             if (obj == null) return false;
421             if (this == obj) return true;
422             if (!(obj instanceof TaskCorners other)) return false;
423 
424             return this.mCornerSize == other.mCornerSize
425                     && this.mLeftTopCornerBounds.equals(other.mLeftTopCornerBounds)
426                     && this.mRightTopCornerBounds.equals(other.mRightTopCornerBounds)
427                     && this.mLeftBottomCornerBounds.equals(other.mLeftBottomCornerBounds)
428                     && this.mRightBottomCornerBounds.equals(other.mRightBottomCornerBounds);
429         }
430 
431         @Override
hashCode()432         public int hashCode() {
433             return Objects.hash(
434                     mCornerSize,
435                     mLeftTopCornerBounds,
436                     mRightTopCornerBounds,
437                     mLeftBottomCornerBounds,
438                     mRightBottomCornerBounds);
439         }
440     }
441 
442     /**
443      * Representation of the drag resize regions at the edges of the window.
444      */
445     private static class TaskEdges {
446         private final @NonNull Rect mTopEdgeBounds;
447         private final @NonNull Rect mLeftEdgeBounds;
448         private final @NonNull Rect mRightEdgeBounds;
449         private final @NonNull Rect mBottomEdgeBounds;
450         private final @NonNull Region mRegion;
451 
TaskEdges(@onNull Size taskSize, int resizeHandleThickness)452         private TaskEdges(@NonNull Size taskSize, int resizeHandleThickness) {
453             // Save touch areas for each edge.
454             mTopEdgeBounds = new Rect(
455                     -resizeHandleThickness,
456                     -resizeHandleThickness,
457                     taskSize.getWidth() + resizeHandleThickness,
458                     0);
459             mLeftEdgeBounds = new Rect(
460                     -resizeHandleThickness,
461                     0,
462                     0,
463                     taskSize.getHeight());
464             mRightEdgeBounds = new Rect(
465                     taskSize.getWidth(),
466                     0,
467                     taskSize.getWidth() + resizeHandleThickness,
468                     taskSize.getHeight());
469             mBottomEdgeBounds = new Rect(
470                     -resizeHandleThickness,
471                     taskSize.getHeight(),
472                     taskSize.getWidth() + resizeHandleThickness,
473                     taskSize.getHeight() + resizeHandleThickness);
474 
475             mRegion = new Region();
476             mRegion.union(mTopEdgeBounds);
477             mRegion.union(mLeftEdgeBounds);
478             mRegion.union(mRightEdgeBounds);
479             mRegion.union(mBottomEdgeBounds);
480         }
481 
482         /**
483          * Returns {@code true} if the edges contain the given point.
484          */
contains(int x, int y)485         private boolean contains(int x, int y) {
486             return mRegion.contains(x, y);
487         }
488 
489         /**
490          * Updates the region to include all four corners.
491          */
union(Region region)492         private void union(Region region) {
493             region.union(mTopEdgeBounds);
494             region.union(mLeftEdgeBounds);
495             region.union(mRightEdgeBounds);
496             region.union(mBottomEdgeBounds);
497         }
498 
499         @Override
toString()500         public String toString() {
501             return "TaskEdges for the"
502                     + " top " + mTopEdgeBounds
503                     + " left " + mLeftEdgeBounds
504                     + " right " + mRightEdgeBounds
505                     + " bottom " + mBottomEdgeBounds;
506         }
507 
508         @Override
equals(Object obj)509         public boolean equals(Object obj) {
510             if (obj == null) return false;
511             if (this == obj) return true;
512             if (!(obj instanceof TaskEdges other)) return false;
513 
514             return this.mTopEdgeBounds.equals(other.mTopEdgeBounds)
515                     && this.mLeftEdgeBounds.equals(other.mLeftEdgeBounds)
516                     && this.mRightEdgeBounds.equals(other.mRightEdgeBounds)
517                     && this.mBottomEdgeBounds.equals(other.mBottomEdgeBounds);
518         }
519 
520         @Override
hashCode()521         public int hashCode() {
522             return Objects.hash(
523                     mTopEdgeBounds,
524                     mLeftEdgeBounds,
525                     mRightEdgeBounds,
526                     mBottomEdgeBounds);
527         }
528     }
529 }
530