1 /*
2  * Copyright (C) 2022 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 android.view;
18 
19 import static android.view.Surface.ROTATION_0;
20 
21 import android.annotation.Nullable;
22 import android.annotation.TestApi;
23 import android.content.res.Resources;
24 import android.content.res.TypedArray;
25 import android.graphics.Matrix;
26 import android.graphics.Path;
27 import android.os.Parcel;
28 import android.os.Parcelable;
29 import android.util.DisplayUtils;
30 import android.util.PathParser;
31 import android.util.RotationUtils;
32 
33 import androidx.annotation.NonNull;
34 
35 import com.android.internal.R;
36 import com.android.internal.annotations.GuardedBy;
37 import com.android.internal.annotations.VisibleForTesting;
38 
39 import java.util.Objects;
40 
41 /**
42  * A class representing the shape of a display. It provides a {@link Path} of the display shape of
43  * the display shape.
44  *
45  * {@link DisplayShape} is immutable.
46  */
47 public final class DisplayShape implements Parcelable {
48 
49     /** @hide */
50     public static final DisplayShape NONE = new DisplayShape("" /* displayShapeSpec */,
51             0 /* displayWidth */, 0 /* displayHeight */, 0 /* physicalPixelDisplaySizeRatio */,
52             0 /* rotation */);
53 
54     /** @hide */
55     @VisibleForTesting
56     public final String mDisplayShapeSpec;
57     private final float mPhysicalPixelDisplaySizeRatio;
58     private final int mDisplayWidth;
59     private final int mDisplayHeight;
60     private final int mRotation;
61     private final int mOffsetX;
62     private final int mOffsetY;
63     private final float mScale;
64 
DisplayShape(@onNull String displayShapeSpec, int displayWidth, int displayHeight, float physicalPixelDisplaySizeRatio, int rotation)65     private DisplayShape(@NonNull String displayShapeSpec, int displayWidth, int displayHeight,
66             float physicalPixelDisplaySizeRatio, int rotation) {
67         this(displayShapeSpec, displayWidth, displayHeight, physicalPixelDisplaySizeRatio,
68                 rotation, 0, 0, 1f);
69     }
70 
DisplayShape(@onNull String displayShapeSpec, int displayWidth, int displayHeight, float physicalPixelDisplaySizeRatio, int rotation, int offsetX, int offsetY, float scale)71     private DisplayShape(@NonNull String displayShapeSpec, int displayWidth, int displayHeight,
72             float physicalPixelDisplaySizeRatio, int rotation, int offsetX, int offsetY,
73             float scale) {
74         mDisplayShapeSpec = displayShapeSpec;
75         mDisplayWidth = displayWidth;
76         mDisplayHeight = displayHeight;
77         mPhysicalPixelDisplaySizeRatio = physicalPixelDisplaySizeRatio;
78         mRotation = rotation;
79         mOffsetX = offsetX;
80         mOffsetY = offsetY;
81         mScale = scale;
82     }
83 
84     /**
85      * @hide
86      */
87     @NonNull
fromResources( @onNull Resources res, @NonNull String displayUniqueId, int physicalDisplayWidth, int physicalDisplayHeight, int displayWidth, int displayHeight)88     public static DisplayShape fromResources(
89             @NonNull Resources res, @NonNull String displayUniqueId, int physicalDisplayWidth,
90             int physicalDisplayHeight, int displayWidth, int displayHeight) {
91         final boolean isScreenRound = RoundedCorners.getBuiltInDisplayIsRound(res, displayUniqueId);
92         final String spec = getSpecString(res, displayUniqueId);
93         if (spec == null || spec.isEmpty()) {
94             return createDefaultDisplayShape(displayWidth, displayHeight, isScreenRound);
95         }
96         final float physicalPixelDisplaySizeRatio = DisplayUtils.getPhysicalPixelDisplaySizeRatio(
97                 physicalDisplayWidth, physicalDisplayHeight, displayWidth, displayHeight);
98         return fromSpecString(spec, physicalPixelDisplaySizeRatio, displayWidth, displayHeight);
99     }
100 
101     /**
102      * @hide
103      */
104     @NonNull
createDefaultDisplayShape( int displayWidth, int displayHeight, boolean isScreenRound)105     public static DisplayShape createDefaultDisplayShape(
106             int displayWidth, int displayHeight, boolean isScreenRound) {
107         return fromSpecString(createDefaultSpecString(displayWidth, displayHeight, isScreenRound),
108                 1f, displayWidth, displayHeight);
109     }
110 
111     /**
112      * @hide
113      */
114     @TestApi
115     @NonNull
fromSpecString(@onNull String spec, float physicalPixelDisplaySizeRatio, int displayWidth, int displayHeight)116     public static DisplayShape fromSpecString(@NonNull String spec,
117             float physicalPixelDisplaySizeRatio, int displayWidth, int displayHeight) {
118         return Cache.getDisplayShape(spec, physicalPixelDisplaySizeRatio, displayWidth,
119                     displayHeight);
120     }
121 
createDefaultSpecString(int displayWidth, int displayHeight, boolean isCircular)122     private static String createDefaultSpecString(int displayWidth, int displayHeight,
123             boolean isCircular) {
124         final String spec;
125         if (isCircular) {
126             final float xRadius = displayWidth / 2f;
127             final float yRadius = displayHeight / 2f;
128             // Draw a circular display shape.
129             spec = "M0," + yRadius
130                     // Draw upper half circle with arcTo command.
131                     + " A" + xRadius + "," + yRadius + " 0 1,1 " + displayWidth + "," + yRadius
132                     // Draw lower half circle with arcTo command.
133                     + " A" + xRadius + "," + yRadius + " 0 1,1 0," + yRadius + " Z";
134         } else {
135             // Draw a rectangular display shape.
136             spec = "M0,0"
137                     // Draw top edge.
138                     + " L" + displayWidth + ",0"
139                     // Draw right edge.
140                     + " L" + displayWidth + "," + displayHeight
141                     // Draw bottom edge.
142                     + " L0," + displayHeight
143                     // Draw left edge by close command which draws a line from current position to
144                     // the initial points (0,0).
145                     + " Z";
146         }
147         return spec;
148     }
149 
150     /**
151      * Gets the display shape svg spec string of a display which is determined by the given display
152      * unique id.
153      *
154      * Loads the default config {@link R.string#config_mainDisplayShape} if
155      * {@link R.array#config_displayUniqueIdArray} is not set.
156      *
157      * @hide
158      */
getSpecString(Resources res, String displayUniqueId)159     public static String getSpecString(Resources res, String displayUniqueId) {
160         final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId);
161         final TypedArray array = res.obtainTypedArray(R.array.config_displayShapeArray);
162         final String spec;
163         if (index >= 0 && index < array.length()) {
164             spec = array.getString(index);
165         } else {
166             spec = res.getString(R.string.config_mainDisplayShape);
167         }
168         array.recycle();
169         return spec;
170     }
171 
172     /**
173      * @hide
174      */
setRotation(int rotation)175     public DisplayShape setRotation(int rotation) {
176         return new DisplayShape(mDisplayShapeSpec, mDisplayWidth, mDisplayHeight,
177                 mPhysicalPixelDisplaySizeRatio, rotation, mOffsetX, mOffsetY, mScale);
178     }
179 
180     /**
181      * @hide
182      */
setOffset(int offsetX, int offsetY)183     public DisplayShape setOffset(int offsetX, int offsetY) {
184         return new DisplayShape(mDisplayShapeSpec, mDisplayWidth, mDisplayHeight,
185                 mPhysicalPixelDisplaySizeRatio, mRotation, offsetX, offsetY, mScale);
186     }
187 
188     /**
189      * @hide
190      */
setScale(float scale)191     public DisplayShape setScale(float scale) {
192         return new DisplayShape(mDisplayShapeSpec, mDisplayWidth, mDisplayHeight,
193                 mPhysicalPixelDisplaySizeRatio, mRotation, mOffsetX, mOffsetY, scale);
194     }
195 
196     @Override
hashCode()197     public int hashCode() {
198         return Objects.hash(mDisplayShapeSpec, mDisplayWidth, mDisplayHeight,
199                 mPhysicalPixelDisplaySizeRatio, mRotation, mOffsetX, mOffsetY, mScale);
200     }
201 
202     @Override
equals(@ullable Object o)203     public boolean equals(@Nullable Object o) {
204         if (o == this) {
205             return true;
206         }
207         if (o instanceof DisplayShape) {
208             DisplayShape ds = (DisplayShape) o;
209             return Objects.equals(mDisplayShapeSpec, ds.mDisplayShapeSpec)
210                     && mDisplayWidth == ds.mDisplayWidth && mDisplayHeight == ds.mDisplayHeight
211                     && mPhysicalPixelDisplaySizeRatio == ds.mPhysicalPixelDisplaySizeRatio
212                     && mRotation == ds.mRotation && mOffsetX == ds.mOffsetX
213                     && mOffsetY == ds.mOffsetY && mScale == ds.mScale;
214         }
215         return false;
216     }
217 
218     @Override
toString()219     public String toString() {
220         return "DisplayShape{"
221                 + " spec=" + mDisplayShapeSpec.hashCode()
222                 + " displayWidth=" + mDisplayWidth
223                 + " displayHeight=" + mDisplayHeight
224                 + " physicalPixelDisplaySizeRatio=" + mPhysicalPixelDisplaySizeRatio
225                 + " rotation=" + mRotation
226                 + " offsetX=" + mOffsetX
227                 + " offsetY=" + mOffsetY
228                 + " scale=" + mScale + "}";
229     }
230 
231     /**
232      * Returns a {@link Path} of the display shape.
233      *
234      * @return a {@link Path} of the display shape.
235      */
236     @NonNull
getPath()237     public Path getPath() {
238         return Cache.getPath(this);
239     }
240 
241     @Override
describeContents()242     public int describeContents() {
243         return 0;
244     }
245 
246     @Override
writeToParcel(@onNull Parcel dest, int flags)247     public void writeToParcel(@NonNull Parcel dest, int flags) {
248         dest.writeString8(mDisplayShapeSpec);
249         dest.writeInt(mDisplayWidth);
250         dest.writeInt(mDisplayHeight);
251         dest.writeFloat(mPhysicalPixelDisplaySizeRatio);
252         dest.writeInt(mRotation);
253         dest.writeInt(mOffsetX);
254         dest.writeInt(mOffsetY);
255         dest.writeFloat(mScale);
256     }
257 
258     public static final @NonNull Creator<DisplayShape> CREATOR = new Creator<DisplayShape>() {
259         @Override
260         public DisplayShape createFromParcel(Parcel in) {
261             final String spec = in.readString8();
262             final int displayWidth = in.readInt();
263             final int displayHeight = in.readInt();
264             final float ratio = in.readFloat();
265             final int rotation = in.readInt();
266             final int offsetX = in.readInt();
267             final int offsetY = in.readInt();
268             final float scale = in.readFloat();
269             return new DisplayShape(spec, displayWidth, displayHeight, ratio, rotation, offsetX,
270                     offsetY, scale);
271         }
272 
273         @Override
274         public DisplayShape[] newArray(int size) {
275             return new DisplayShape[size];
276         }
277     };
278 
279     private static final class Cache {
280         private static final Object CACHE_LOCK = new Object();
281 
282         @GuardedBy("CACHE_LOCK")
283         private static String sCachedSpec;
284         @GuardedBy("CACHE_LOCK")
285         private static int sCachedDisplayWidth;
286         @GuardedBy("CACHE_LOCK")
287         private static int sCachedDisplayHeight;
288         @GuardedBy("CACHE_LOCK")
289         private static float sCachedPhysicalPixelDisplaySizeRatio;
290         @GuardedBy("CACHE_LOCK")
291         private static DisplayShape sCachedDisplayShape;
292 
293         @GuardedBy("CACHE_LOCK")
294         private static DisplayShape sCacheForPath;
295         @GuardedBy("CACHE_LOCK")
296         private static Path sCachedPath;
297 
getDisplayShape(String spec, float physicalPixelDisplaySizeRatio, int displayWidth, int displayHeight)298         static DisplayShape getDisplayShape(String spec, float physicalPixelDisplaySizeRatio,
299                 int displayWidth, int displayHeight) {
300             synchronized (CACHE_LOCK) {
301                 if (spec.equals(sCachedSpec)
302                         && sCachedDisplayWidth == displayWidth
303                         && sCachedDisplayHeight == displayHeight
304                         && sCachedPhysicalPixelDisplaySizeRatio == physicalPixelDisplaySizeRatio) {
305                     return sCachedDisplayShape;
306                 }
307             }
308 
309             final DisplayShape shape = new DisplayShape(spec, displayWidth, displayHeight,
310                     physicalPixelDisplaySizeRatio, ROTATION_0);
311 
312             synchronized (CACHE_LOCK) {
313                 sCachedSpec = spec;
314                 sCachedDisplayWidth = displayWidth;
315                 sCachedDisplayHeight = displayHeight;
316                 sCachedPhysicalPixelDisplaySizeRatio = physicalPixelDisplaySizeRatio;
317                 sCachedDisplayShape = shape;
318             }
319             return shape;
320         }
321 
getPath(@onNull DisplayShape shape)322         static Path getPath(@NonNull DisplayShape shape) {
323             synchronized (CACHE_LOCK) {
324                 if (shape.equals(sCacheForPath)) {
325                     return sCachedPath;
326                 }
327             }
328 
329             final Path path = PathParser.createPathFromPathData(shape.mDisplayShapeSpec);
330 
331             if (!path.isEmpty()) {
332                 final Matrix matrix = new Matrix();
333                 if (shape.mRotation != ROTATION_0) {
334                     RotationUtils.transformPhysicalToLogicalCoordinates(
335                             shape.mRotation, shape.mDisplayWidth, shape.mDisplayHeight, matrix);
336                 }
337                 if (shape.mPhysicalPixelDisplaySizeRatio != 1f) {
338                     matrix.preScale(shape.mPhysicalPixelDisplaySizeRatio,
339                             shape.mPhysicalPixelDisplaySizeRatio);
340                 }
341                 if (shape.mOffsetX != 0 || shape.mOffsetY != 0) {
342                     matrix.postTranslate(shape.mOffsetX, shape.mOffsetY);
343                 }
344                 if (shape.mScale != 1f) {
345                     matrix.postScale(shape.mScale, shape.mScale);
346                 }
347                 path.transform(matrix);
348             }
349 
350             synchronized (CACHE_LOCK) {
351                 sCacheForPath = shape;
352                 sCachedPath = path;
353             }
354             return path;
355         }
356     }
357 }
358