1 package com.android.systemui.shared.recents.utilities;
2 
3 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
4 import static android.view.Surface.ROTATION_180;
5 import static android.view.Surface.ROTATION_270;
6 import static android.view.Surface.ROTATION_90;
7 
8 import android.graphics.Matrix;
9 import android.graphics.Rect;
10 import android.graphics.RectF;
11 
12 import com.android.systemui.shared.recents.model.ThumbnailData;
13 import com.android.wm.shell.util.SplitBounds;
14 
15 /**
16  * Utility class to position the thumbnail in the TaskView
17  */
18 public class PreviewPositionHelper {
19 
20     public static final float MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT = 0.1f;
21 
22     /**
23      * Specifies that a stage is positioned at the top half of the screen if
24      * in portrait mode or at the left half of the screen if in landscape mode.
25      * TODO(b/254378592): Remove after consolidation
26      */
27     public static final int STAGE_POSITION_TOP_OR_LEFT = 0;
28 
29     /**
30      * Specifies that a stage is positioned at the bottom half of the screen if
31      * in portrait mode or at the right half of the screen if in landscape mode.
32      * TODO(b/254378592): Remove after consolidation
33      */
34     public static final int STAGE_POSITION_BOTTOM_OR_RIGHT = 1;
35 
36     private final Matrix mMatrix = new Matrix();
37     private boolean mIsOrientationChanged;
38     private SplitBounds mSplitBounds;
39     private int mDesiredStagePosition;
40 
getMatrix()41     public Matrix getMatrix() {
42         return mMatrix;
43     }
44 
setOrientationChanged(boolean orientationChanged)45     public void setOrientationChanged(boolean orientationChanged) {
46         mIsOrientationChanged = orientationChanged;
47     }
48 
isOrientationChanged()49     public boolean isOrientationChanged() {
50         return mIsOrientationChanged;
51     }
52 
setSplitBounds(SplitBounds splitBounds, int desiredStagePosition)53     public void setSplitBounds(SplitBounds splitBounds, int desiredStagePosition) {
54         mSplitBounds = splitBounds;
55         mDesiredStagePosition = desiredStagePosition;
56     }
57 
58     /**
59      * Updates the matrix based on the provided parameters
60      */
updateThumbnailMatrix(Rect thumbnailBounds, ThumbnailData thumbnailData, int canvasWidth, int canvasHeight, int screenWidthPx, int screenHeightPx, int taskbarSize, boolean isLargeScreen, int currentRotation, boolean isRtl)61     public void updateThumbnailMatrix(Rect thumbnailBounds, ThumbnailData thumbnailData,
62             int canvasWidth, int canvasHeight, int screenWidthPx, int screenHeightPx,
63             int taskbarSize, boolean isLargeScreen,
64             int currentRotation, boolean isRtl) {
65         boolean isRotated = false;
66         boolean isOrientationDifferent;
67 
68         int thumbnailRotation = thumbnailData.rotation;
69         int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation);
70         RectF thumbnailClipHint = new RectF();
71         float scale = thumbnailData.scale;
72         final float thumbnailScale;
73 
74         // Landscape vs portrait change.
75         // Note: Disable rotation in grid layout.
76         boolean windowingModeSupportsRotation =
77                 thumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN && !isLargeScreen;
78         isOrientationDifferent = isOrientationChange(deltaRotate)
79                 && windowingModeSupportsRotation;
80         if (canvasWidth == 0 || canvasHeight == 0 || scale == 0) {
81             // If we haven't measured , skip the thumbnail drawing and only draw the background
82             // color
83             thumbnailScale = 0f;
84         } else {
85             // Rotate the screenshot if not in multi-window mode
86             isRotated = deltaRotate > 0 && windowingModeSupportsRotation;
87 
88             float surfaceWidth = thumbnailBounds.width() / scale;
89             float surfaceHeight = thumbnailBounds.height() / scale;
90             float availableWidth = surfaceWidth;
91             float availableHeight = surfaceHeight;
92 
93             float canvasAspect = canvasWidth / (float) canvasHeight;
94             float availableAspect = isRotated
95                     ? availableHeight / availableWidth
96                     : availableWidth / availableHeight;
97             boolean isAspectLargelyDifferent =
98                     Utilities.isRelativePercentDifferenceGreaterThan(canvasAspect,
99                             availableAspect, MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT);
100             if (isRotated && isAspectLargelyDifferent) {
101                 // Do not rotate thumbnail if it would not improve fit
102                 isRotated = false;
103                 isOrientationDifferent = false;
104             }
105 
106             if (isAspectLargelyDifferent) {
107                 // Crop letterbox insets if insets isn't already clipped
108                 thumbnailClipHint.left = thumbnailData.letterboxInsets.left;
109                 thumbnailClipHint.right = thumbnailData.letterboxInsets.right;
110                 thumbnailClipHint.top = thumbnailData.letterboxInsets.top;
111                 thumbnailClipHint.bottom = thumbnailData.letterboxInsets.bottom;
112                 availableWidth = surfaceWidth
113                         - (thumbnailClipHint.left + thumbnailClipHint.right);
114                 availableHeight = surfaceHeight
115                         - (thumbnailClipHint.top + thumbnailClipHint.bottom);
116             }
117 
118             final float targetW, targetH;
119             if (isOrientationDifferent) {
120                 targetW = canvasHeight;
121                 targetH = canvasWidth;
122             } else {
123                 targetW = canvasWidth;
124                 targetH = canvasHeight;
125             }
126             float targetAspect = targetW / targetH;
127 
128             // Update the clipHint such that
129             //   > the final clipped position has same aspect ratio as requested by canvas
130             //   > first fit the width and crop the extra height
131             //   > if that will leave empty space, fit the height and crop the width instead
132             float croppedWidth = availableWidth;
133             float croppedHeight = croppedWidth / targetAspect;
134             if (croppedHeight > availableHeight) {
135                 croppedHeight = availableHeight;
136                 if (croppedHeight < targetH) {
137                     croppedHeight = Math.min(targetH, surfaceHeight);
138                 }
139                 croppedWidth = croppedHeight * targetAspect;
140 
141                 // One last check in case the task aspect radio messed up something
142                 if (croppedWidth > surfaceWidth) {
143                     croppedWidth = surfaceWidth;
144                     croppedHeight = croppedWidth / targetAspect;
145                 }
146             }
147 
148             // Update the clip hints. Align to 0,0, crop the remaining.
149             if (isRtl) {
150                 thumbnailClipHint.left += availableWidth - croppedWidth;
151                 if (thumbnailClipHint.right < 0) {
152                     thumbnailClipHint.left += thumbnailClipHint.right;
153                     thumbnailClipHint.right = 0;
154                 }
155             } else {
156                 thumbnailClipHint.right += availableWidth - croppedWidth;
157                 if (thumbnailClipHint.left < 0) {
158                     thumbnailClipHint.right += thumbnailClipHint.left;
159                     thumbnailClipHint.left = 0;
160                 }
161             }
162             thumbnailClipHint.bottom += availableHeight - croppedHeight;
163             if (thumbnailClipHint.top < 0) {
164                 thumbnailClipHint.bottom += thumbnailClipHint.top;
165                 thumbnailClipHint.top = 0;
166             } else if (thumbnailClipHint.bottom < 0) {
167                 thumbnailClipHint.top += thumbnailClipHint.bottom;
168                 thumbnailClipHint.bottom = 0;
169             }
170 
171             thumbnailScale = targetW / (croppedWidth * scale);
172         }
173 
174         if (!isRotated) {
175             mMatrix.setTranslate(
176                     -thumbnailClipHint.left * scale,
177                     -thumbnailClipHint.top * scale);
178         } else {
179             setThumbnailRotation(deltaRotate, thumbnailBounds);
180         }
181 
182         mMatrix.postScale(thumbnailScale, thumbnailScale);
183         mIsOrientationChanged = isOrientationDifferent;
184     }
185 
getRotationDelta(int oldRotation, int newRotation)186     private int getRotationDelta(int oldRotation, int newRotation) {
187         int delta = newRotation - oldRotation;
188         if (delta < 0) delta += 4;
189         return delta;
190     }
191 
192     /**
193      * @param deltaRotation the number of 90 degree turns from the current orientation
194      * @return {@code true} if the change in rotation results in a shift from landscape to
195      * portrait or vice versa, {@code false} otherwise
196      */
isOrientationChange(int deltaRotation)197     private boolean isOrientationChange(int deltaRotation) {
198         return deltaRotation == ROTATION_90 || deltaRotation == ROTATION_270;
199     }
200 
setThumbnailRotation(int deltaRotate, Rect thumbnailPosition)201     private void setThumbnailRotation(int deltaRotate, Rect thumbnailPosition) {
202         float translateX = 0;
203         float translateY = 0;
204 
205         mMatrix.setRotate(90 * deltaRotate);
206         switch (deltaRotate) { /* Counter-clockwise */
207             case ROTATION_90:
208                 translateX = thumbnailPosition.height();
209                 break;
210             case ROTATION_270:
211                 translateY = thumbnailPosition.width();
212                 break;
213             case ROTATION_180:
214                 translateX = thumbnailPosition.width();
215                 translateY = thumbnailPosition.height();
216                 break;
217         }
218         mMatrix.postTranslate(translateX, translateY);
219     }
220 }
221