• 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 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