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 android.app.ActivityManager.RunningTaskInfo 20 import android.graphics.PointF 21 import android.graphics.Rect 22 import com.android.internal.annotations.VisibleForTesting 23 import com.android.wm.shell.desktopmode.calculateAspectRatio 24 import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM 25 import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT 26 import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT 27 import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP 28 import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED 29 import com.android.wm.shell.windowdecor.DragPositioningCallback.CtrlType 30 import kotlin.math.abs 31 32 /** 33 * [AbstractTaskPositionerDecorator] implementation for validating the coordinates associated with a 34 * drag action, to maintain a fixed aspect ratio before being used by the task positioner. 35 */ 36 class FixedAspectRatioTaskPositionerDecorator ( 37 private val windowDecoration: DesktopModeWindowDecoration, 38 decoratedTaskPositioner: TaskPositioner 39 ) : AbstractTaskPositionerDecorator(decoratedTaskPositioner) { 40 41 private var originalCtrlType = CTRL_TYPE_UNDEFINED 42 private var edgeResizeCtrlType = CTRL_TYPE_UNDEFINED 43 private val lastRepositionedBounds = Rect() 44 private val startingPoint = PointF() 45 private val lastValidPoint = PointF() 46 private var startingAspectRatio = 0f 47 private var isTaskPortrait = false 48 onDragPositioningStartnull49 override fun onDragPositioningStart( 50 @CtrlType ctrlType: Int, displayId: Int, x: Float, y: Float): Rect { 51 originalCtrlType = ctrlType 52 if (!requiresFixedAspectRatio()) { 53 return super.onDragPositioningStart(originalCtrlType, displayId, x, y) 54 } 55 56 lastRepositionedBounds.set(getBounds(windowDecoration.mTaskInfo)) 57 startingPoint.set(x, y) 58 lastValidPoint.set(x, y) 59 val startingBoundWidth = lastRepositionedBounds.width() 60 val startingBoundHeight = lastRepositionedBounds.height() 61 startingAspectRatio = calculateAspectRatio(windowDecoration.mTaskInfo) 62 isTaskPortrait = startingBoundWidth <= startingBoundHeight 63 64 lastRepositionedBounds.set( 65 when (originalCtrlType) { 66 // If resize in an edge resize, adjust ctrlType passed to onDragPositioningStart() to 67 // mimic a corner resize instead. As at lest two adjacent edges need to be resized 68 // in relation to each other to maintain the apps aspect ratio. The additional adjacent 69 // edge is selected based on its proximity (closest) to the start of the drag. 70 CTRL_TYPE_LEFT, CTRL_TYPE_RIGHT -> { 71 val verticalMidPoint = lastRepositionedBounds.top + (startingBoundHeight / 2) 72 edgeResizeCtrlType = originalCtrlType + 73 if (y < verticalMidPoint) CTRL_TYPE_TOP else CTRL_TYPE_BOTTOM 74 super.onDragPositioningStart(edgeResizeCtrlType, displayId, x, y) 75 } 76 CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM -> { 77 val horizontalMidPoint = lastRepositionedBounds.left + (startingBoundWidth / 2) 78 edgeResizeCtrlType = originalCtrlType + 79 if (x < horizontalMidPoint) CTRL_TYPE_LEFT else CTRL_TYPE_RIGHT 80 super.onDragPositioningStart(edgeResizeCtrlType, displayId, x, y) 81 } 82 // If resize is corner resize, no alteration to the ctrlType needs to be made. 83 else -> { 84 edgeResizeCtrlType = CTRL_TYPE_UNDEFINED 85 super.onDragPositioningStart(originalCtrlType, displayId, x, y) 86 } 87 } 88 ) 89 return lastRepositionedBounds 90 } 91 onDragPositioningMovenull92 override fun onDragPositioningMove(displayId: Int, x: Float, y: Float): Rect { 93 if (!requiresFixedAspectRatio()) { 94 return super.onDragPositioningMove(displayId, x, y) 95 } 96 97 val diffX = x - lastValidPoint.x 98 val diffY = y - lastValidPoint.y 99 when (originalCtrlType) { 100 CTRL_TYPE_BOTTOM + CTRL_TYPE_RIGHT, CTRL_TYPE_TOP + CTRL_TYPE_LEFT -> { 101 if ((diffX > 0 && diffY > 0) || (diffX < 0 && diffY < 0)) { 102 // Drag coordinate falls within valid region (90 - 180 degrees or 270- 360 103 // degrees from the corner the previous valid point). Allow resize with adjusted 104 // coordinates to maintain aspect ratio. 105 lastRepositionedBounds.set(dragAdjustedMove(displayId, x, y)) 106 } 107 } 108 CTRL_TYPE_BOTTOM + CTRL_TYPE_LEFT, CTRL_TYPE_TOP + CTRL_TYPE_RIGHT -> { 109 if ((diffX > 0 && diffY < 0) || (diffX < 0 && diffY > 0)) { 110 // Drag coordinate falls within valid region (180 - 270 degrees or 0 - 90 111 // degrees from the corner the previous valid point). Allow resize with adjusted 112 // coordinates to maintain aspect ratio. 113 lastRepositionedBounds.set(dragAdjustedMove(displayId, x, y)) 114 } 115 } 116 CTRL_TYPE_LEFT, CTRL_TYPE_RIGHT -> { 117 // If resize is on left or right edge, always adjust the y coordinate. 118 val adjustedY = getScaledChangeForY(x) 119 lastValidPoint.set(x, adjustedY) 120 lastRepositionedBounds.set(super.onDragPositioningMove(displayId, x, adjustedY)) 121 } 122 CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM -> { 123 // If resize is on top or bottom edge, always adjust the x coordinate. 124 val adjustedX = getScaledChangeForX(y) 125 lastValidPoint.set(adjustedX, y) 126 lastRepositionedBounds.set(super.onDragPositioningMove(displayId, adjustedX, y)) 127 } 128 } 129 return lastRepositionedBounds 130 } 131 onDragPositioningEndnull132 override fun onDragPositioningEnd(displayId: Int, x: Float, y: Float): Rect { 133 if (!requiresFixedAspectRatio()) { 134 return super.onDragPositioningEnd(displayId, x, y) 135 } 136 137 val diffX = x - lastValidPoint.x 138 val diffY = y - lastValidPoint.y 139 140 when (originalCtrlType) { 141 CTRL_TYPE_BOTTOM + CTRL_TYPE_RIGHT, CTRL_TYPE_TOP + CTRL_TYPE_LEFT -> { 142 if ((diffX > 0 && diffY > 0) || (diffX < 0 && diffY < 0)) { 143 // Drag coordinate falls within valid region (90 - 180 degrees or 270- 360 144 // degrees from the corner the previous valid point). End resize with adjusted 145 // coordinates to maintain aspect ratio. 146 return dragAdjustedEnd(displayId, x, y) 147 } 148 // If end of resize is not within valid region, end resize from last valid 149 // coordinates. 150 return super.onDragPositioningEnd(displayId, lastValidPoint.x, lastValidPoint.y) 151 } 152 CTRL_TYPE_BOTTOM + CTRL_TYPE_LEFT, CTRL_TYPE_TOP + CTRL_TYPE_RIGHT -> { 153 if ((diffX > 0 && diffY < 0) || (diffX < 0 && diffY > 0)) { 154 // Drag coordinate falls within valid region (180 - 260 degrees or 0 - 90 155 // degrees from the corner the previous valid point). End resize with adjusted 156 // coordinates to maintain aspect ratio. 157 return dragAdjustedEnd(displayId, x, y) 158 } 159 // If end of resize is not within valid region, end resize from last valid 160 // coordinates. 161 return super.onDragPositioningEnd(displayId, lastValidPoint.x, lastValidPoint.y) 162 } 163 CTRL_TYPE_LEFT, CTRL_TYPE_RIGHT -> { 164 // If resize is on left or right edge, always adjust the y coordinate. 165 return super.onDragPositioningEnd(displayId, x, getScaledChangeForY(x)) 166 } 167 CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM -> { 168 // If resize is on top or bottom edge, always adjust the x coordinate. 169 return super.onDragPositioningEnd(displayId, getScaledChangeForX(y), y) 170 } 171 else -> { 172 return super.onDragPositioningEnd(displayId, x, y) 173 } 174 } 175 } 176 dragAdjustedMovenull177 private fun dragAdjustedMove(displayId: Int, x: Float, y: Float): Rect { 178 val absDiffX = abs(x - lastValidPoint.x) 179 val absDiffY = abs(y - lastValidPoint.y) 180 if (absDiffY < absDiffX) { 181 lastValidPoint.set(getScaledChangeForX(y), y) 182 return super.onDragPositioningMove(displayId, getScaledChangeForX(y), y) 183 } 184 lastValidPoint.set(x, getScaledChangeForY(x)) 185 return super.onDragPositioningMove(displayId, x, getScaledChangeForY(x)) 186 } 187 dragAdjustedEndnull188 private fun dragAdjustedEnd(displayId: Int, x: Float, y: Float): Rect { 189 val absDiffX = abs(x - lastValidPoint.x) 190 val absDiffY = abs(y - lastValidPoint.y) 191 if (absDiffY < absDiffX) { 192 return super.onDragPositioningEnd(displayId, getScaledChangeForX(y), y) 193 } 194 return super.onDragPositioningEnd(displayId, x, getScaledChangeForY(x)) 195 } 196 197 /** 198 * Calculate the required change in the y dimension, given the change in the x dimension, to 199 * maintain the applications starting aspect ratio when resizing to a given x coordinate. 200 */ getScaledChangeForYnull201 private fun getScaledChangeForY(x: Float): Float { 202 val changeXDimension = x - startingPoint.x 203 val changeYDimension = if (isTaskPortrait) { 204 changeXDimension * startingAspectRatio 205 } else { 206 changeXDimension / startingAspectRatio 207 } 208 if (originalCtrlType.isBottomRightOrTopLeftCorner() 209 || edgeResizeCtrlType.isBottomRightOrTopLeftCorner()) { 210 return startingPoint.y + changeYDimension 211 } 212 return startingPoint.y - changeYDimension 213 } 214 215 /** 216 * Calculate the required change in the x dimension, given the change in the y dimension, to 217 * maintain the applications starting aspect ratio when resizing to a given y coordinate. 218 */ getScaledChangeForXnull219 private fun getScaledChangeForX(y: Float): Float { 220 val changeYDimension = y - startingPoint.y 221 val changeXDimension = if (isTaskPortrait) { 222 changeYDimension / startingAspectRatio 223 } else { 224 changeYDimension * startingAspectRatio 225 } 226 if (originalCtrlType.isBottomRightOrTopLeftCorner() 227 || edgeResizeCtrlType.isBottomRightOrTopLeftCorner()) { 228 return startingPoint.x + changeXDimension 229 } 230 return startingPoint.x - changeXDimension 231 } 232 233 /** 234 * If the action being triggered originated from the bottom right or top left corner of the 235 * window. 236 */ isBottomRightOrTopLeftCornernull237 private fun @receiver:CtrlType Int.isBottomRightOrTopLeftCorner(): Boolean { 238 return this == CTRL_TYPE_BOTTOM + CTRL_TYPE_RIGHT || this == CTRL_TYPE_TOP + CTRL_TYPE_LEFT 239 } 240 241 /** 242 * If the action being triggered is a resize action. 243 */ isResizingnull244 private fun @receiver:CtrlType Int.isResizing(): Boolean { 245 return (this and CTRL_TYPE_TOP) != 0 || (this and CTRL_TYPE_BOTTOM) != 0 246 || (this and CTRL_TYPE_LEFT) != 0 || (this and CTRL_TYPE_RIGHT) != 0 247 } 248 249 /** 250 * Whether the aspect ratio of the activity needs to be maintained during the current drag 251 * action. If the current action is not a resize (there is no bounds change) so the aspect ratio 252 * is already maintained and does not need handling here. If the activity is resizeable, it 253 * can handle aspect ratio changes itself so again we do not need to handle it here. 254 */ requiresFixedAspectRationull255 private fun requiresFixedAspectRatio(): Boolean { 256 return originalCtrlType.isResizing() && !windowDecoration.mTaskInfo.isResizeable 257 } 258 259 @VisibleForTesting getBoundsnull260 fun getBounds(taskInfo: RunningTaskInfo): Rect { 261 return taskInfo.configuration.windowConfiguration.bounds 262 } 263 } 264