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, boolean isLargeScreen, int currentRotation, boolean isRtl)61     public void updateThumbnailMatrix(Rect thumbnailBounds, ThumbnailData thumbnailData,
62             int canvasWidth, int canvasHeight, boolean isLargeScreen, int currentRotation,
63             boolean isRtl) {
64         boolean isRotated = false;
65         boolean isOrientationDifferent;
66 
67         int thumbnailRotation = thumbnailData.rotation;
68         int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation);
69         RectF thumbnailClipHint = new RectF();
70         float scale = thumbnailData.scale;
71         final float thumbnailScale;
72 
73         // Landscape vs portrait change.
74         // Note: Disable rotation in grid layout.
75         boolean windowingModeSupportsRotation =
76                 thumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN && !isLargeScreen;
77         isOrientationDifferent = isOrientationChange(deltaRotate)
78                 && windowingModeSupportsRotation;
79         if (canvasWidth == 0 || canvasHeight == 0 || scale == 0) {
80             // If we haven't measured , skip the thumbnail drawing and only draw the background
81             // color
82             thumbnailScale = 0f;
83         } else {
84             // Rotate the screenshot if not in multi-window mode
85             isRotated = deltaRotate > 0 && windowingModeSupportsRotation;
86 
87             float surfaceWidth = thumbnailBounds.width() / scale;
88             float surfaceHeight = thumbnailBounds.height() / scale;
89             float availableWidth = surfaceWidth;
90             float availableHeight = surfaceHeight;
91 
92             float canvasAspect = canvasWidth / (float) canvasHeight;
93             float availableAspect = isRotated
94                     ? availableHeight / availableWidth
95                     : availableWidth / availableHeight;
96             boolean isAspectLargelyDifferent =
97                     Utilities.isRelativePercentDifferenceGreaterThan(canvasAspect,
98                             availableAspect, MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT);
99             if (isRotated && isAspectLargelyDifferent) {
100                 // Do not rotate thumbnail if it would not improve fit
101                 isRotated = false;
102                 isOrientationDifferent = false;
103             }
104 
105             if (isAspectLargelyDifferent) {
106                 // Crop letterbox insets if insets isn't already clipped
107                 thumbnailClipHint.left = thumbnailData.letterboxInsets.left;
108                 thumbnailClipHint.right = thumbnailData.letterboxInsets.right;
109                 thumbnailClipHint.top = thumbnailData.letterboxInsets.top;
110                 thumbnailClipHint.bottom = thumbnailData.letterboxInsets.bottom;
111                 availableWidth = surfaceWidth
112                         - (thumbnailClipHint.left + thumbnailClipHint.right);
113                 availableHeight = surfaceHeight
114                         - (thumbnailClipHint.top + thumbnailClipHint.bottom);
115             }
116 
117             final float targetW, targetH;
118             if (isOrientationDifferent) {
119                 targetW = canvasHeight;
120                 targetH = canvasWidth;
121             } else {
122                 targetW = canvasWidth;
123                 targetH = canvasHeight;
124             }
125             float targetAspect = targetW / targetH;
126 
127             // Update the clipHint such that
128             //   > the final clipped position has same aspect ratio as requested by canvas
129             //   > first fit the width and crop the extra height
130             //   > if that will leave empty space, fit the height and crop the width instead
131             float croppedWidth = availableWidth;
132             float croppedHeight = croppedWidth / targetAspect;
133             if (croppedHeight > availableHeight) {
134                 croppedHeight = availableHeight;
135                 if (croppedHeight < targetH) {
136                     croppedHeight = Math.min(targetH, surfaceHeight);
137                 }
138                 croppedWidth = croppedHeight * targetAspect;
139 
140                 // One last check in case the task aspect radio messed up something
141                 if (croppedWidth > surfaceWidth) {
142                     croppedWidth = surfaceWidth;
143                     croppedHeight = croppedWidth / targetAspect;
144                 }
145             }
146 
147             // Update the clip hints. Align to 0,0, crop the remaining.
148             if (isRtl) {
149                 thumbnailClipHint.left += availableWidth - croppedWidth;
150                 if (thumbnailClipHint.right < 0) {
151                     thumbnailClipHint.left += thumbnailClipHint.right;
152                     thumbnailClipHint.right = 0;
153                 }
154             } else {
155                 thumbnailClipHint.right += availableWidth - croppedWidth;
156                 if (thumbnailClipHint.left < 0) {
157                     thumbnailClipHint.right += thumbnailClipHint.left;
158                     thumbnailClipHint.left = 0;
159                 }
160             }
161             thumbnailClipHint.bottom += availableHeight - croppedHeight;
162             if (thumbnailClipHint.top < 0) {
163                 thumbnailClipHint.bottom += thumbnailClipHint.top;
164                 thumbnailClipHint.top = 0;
165             } else if (thumbnailClipHint.bottom < 0) {
166                 thumbnailClipHint.top += thumbnailClipHint.bottom;
167                 thumbnailClipHint.bottom = 0;
168             }
169 
170             thumbnailScale = targetW / (croppedWidth * scale);
171         }
172 
173         if (!isRotated) {
174             mMatrix.setTranslate(
175                     -thumbnailClipHint.left * scale,
176                     -thumbnailClipHint.top * scale);
177         } else {
178             setThumbnailRotation(deltaRotate, thumbnailBounds);
179         }
180 
181         mMatrix.postScale(thumbnailScale, thumbnailScale);
182         mIsOrientationChanged = isOrientationDifferent;
183     }
184 
getRotationDelta(int oldRotation, int newRotation)185     private int getRotationDelta(int oldRotation, int newRotation) {
186         int delta = newRotation - oldRotation;
187         if (delta < 0) delta += 4;
188         return delta;
189     }
190 
191     /**
192      * @param deltaRotation the number of 90 degree turns from the current orientation
193      * @return {@code true} if the change in rotation results in a shift from landscape to
194      * portrait or vice versa, {@code false} otherwise
195      */
isOrientationChange(int deltaRotation)196     private boolean isOrientationChange(int deltaRotation) {
197         return deltaRotation == ROTATION_90 || deltaRotation == ROTATION_270;
198     }
199 
setThumbnailRotation(int deltaRotate, Rect thumbnailPosition)200     private void setThumbnailRotation(int deltaRotate, Rect thumbnailPosition) {
201         float translateX = 0;
202         float translateY = 0;
203 
204         mMatrix.setRotate(90 * deltaRotate);
205         switch (deltaRotate) { /* Counter-clockwise */
206             case ROTATION_90:
207                 translateX = thumbnailPosition.height();
208                 break;
209             case ROTATION_270:
210                 translateY = thumbnailPosition.width();
211                 break;
212             case ROTATION_180:
213                 translateX = thumbnailPosition.width();
214                 translateY = thumbnailPosition.height();
215                 break;
216         }
217         mMatrix.postTranslate(translateX, translateY);
218     }
219 }
220