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