• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.Gravity.BOTTOM;
20 import static android.view.Gravity.LEFT;
21 import static android.view.Gravity.RIGHT;
22 import static android.view.Gravity.TOP;
23 
24 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
25 
26 import android.annotation.NonNull;
27 import android.annotation.Nullable;
28 import android.graphics.Insets;
29 import android.graphics.Matrix;
30 import android.graphics.Path;
31 import android.graphics.Rect;
32 import android.graphics.RectF;
33 import android.graphics.Region;
34 import android.text.TextUtils;
35 import android.util.Log;
36 import android.util.PathParser;
37 
38 import com.android.internal.annotations.VisibleForTesting;
39 
40 import java.util.Locale;
41 import java.util.Objects;
42 
43 /**
44  * In order to accept the cutout specification for all of edges in devices, the specification
45  * parsing method is extracted from
46  * {@link android.view.DisplayCutout#fromResourcesRectApproximation(Resources, int, int)} to be
47  * the specified class for parsing the specification.
48  * BNF definition:
49  * <ul>
50  *      <li>Cutouts Specification = ([Cutout Delimiter],Cutout Specification) {...}, [Dp] ; </li>
51  *      <li>Cutout Specification  = [Vertical Position], (SVG Path Element), [Horizontal Position]
52  *                                  [Bind Cutout] ;</li>
53  *      <li>Vertical Position     = "@bottom" | "@center_vertical" ;</li>
54  *      <li>Horizontal Position   = "@left" | "@right" ;</li>
55  *      <li>Bind Cutout           = "@bind_left_cutout" | "@bind_right_cutout" ;</li>
56  *      <li>Cutout Delimiter      = "@cutout" ;</li>
57  *      <li>Dp                    = "@dp"</li>
58  * </ul>
59  *
60  * <ul>
61  *     <li>Vertical position is top by default if there is neither "@bottom" nor "@center_vertical"
62  *     </li>
63  *     <li>Horizontal position is center horizontal by default if there is neither "@left" nor
64  *     "@right".</li>
65  *     <li>@bottom make the cutout piece bind to bottom edge.</li>
66  *     <li>both of @bind_left_cutout and @bind_right_cutout are use to claim the cutout belong to
67  *     left or right edge cutout.</li>
68  * </ul>
69  *
70  * @hide
71  */
72 @VisibleForTesting(visibility = PACKAGE)
73 public class CutoutSpecification {
74     private static final String TAG = "CutoutSpecification";
75     private static final boolean DEBUG = false;
76 
77     private static final int MINIMAL_ACCEPTABLE_PATH_LENGTH = "H1V1Z".length();
78 
79     private static final char MARKER_START_CHAR = '@';
80     private static final String DP_MARKER = MARKER_START_CHAR + "dp";
81 
82     private static final String BOTTOM_MARKER = MARKER_START_CHAR + "bottom";
83     private static final String RIGHT_MARKER = MARKER_START_CHAR + "right";
84     private static final String LEFT_MARKER = MARKER_START_CHAR + "left";
85     private static final String CUTOUT_MARKER = MARKER_START_CHAR + "cutout";
86     private static final String CENTER_VERTICAL_MARKER = MARKER_START_CHAR + "center_vertical";
87 
88     /* By default, it's top bound cutout. That's why TOP_BOUND_CUTOUT_MARKER is not defined */
89     private static final String BIND_RIGHT_CUTOUT_MARKER = MARKER_START_CHAR + "bind_right_cutout";
90     private static final String BIND_LEFT_CUTOUT_MARKER = MARKER_START_CHAR + "bind_left_cutout";
91 
92     private final Path mPath;
93     private final Rect mLeftBound;
94     private final Rect mTopBound;
95     private final Rect mRightBound;
96     private final Rect mBottomBound;
97     private Insets mInsets;
98 
CutoutSpecification(@onNull Parser parser)99     private CutoutSpecification(@NonNull Parser parser) {
100         mPath = parser.mPath;
101         mLeftBound = parser.mLeftBound;
102         mTopBound = parser.mTopBound;
103         mRightBound = parser.mRightBound;
104         mBottomBound = parser.mBottomBound;
105         mInsets = parser.mInsets;
106 
107         applyPhysicalPixelDisplaySizeRatio(parser.mPhysicalPixelDisplaySizeRatio);
108 
109         if (DEBUG) {
110             Log.d(TAG, String.format(Locale.ENGLISH,
111                     "left cutout = %s, top cutout = %s, right cutout = %s, bottom cutout = %s",
112                     mLeftBound != null ? mLeftBound.toString() : "",
113                     mTopBound != null ? mTopBound.toString() : "",
114                     mRightBound != null ? mRightBound.toString() : "",
115                     mBottomBound != null ? mBottomBound.toString() : ""));
116         }
117     }
118 
applyPhysicalPixelDisplaySizeRatio(float physicalPixelDisplaySizeRatio)119     private void applyPhysicalPixelDisplaySizeRatio(float physicalPixelDisplaySizeRatio) {
120         if (physicalPixelDisplaySizeRatio == 1f) {
121             return;
122         }
123 
124         if (mPath != null && !mPath.isEmpty()) {
125             final Matrix matrix = new Matrix();
126             matrix.postScale(physicalPixelDisplaySizeRatio, physicalPixelDisplaySizeRatio);
127             mPath.transform(matrix);
128         }
129 
130         scaleBounds(mLeftBound, physicalPixelDisplaySizeRatio);
131         scaleBounds(mTopBound, physicalPixelDisplaySizeRatio);
132         scaleBounds(mRightBound, physicalPixelDisplaySizeRatio);
133         scaleBounds(mBottomBound, physicalPixelDisplaySizeRatio);
134         mInsets = scaleInsets(mInsets, physicalPixelDisplaySizeRatio);
135     }
136 
scaleBounds(Rect r, float ratio)137     private void scaleBounds(Rect r, float ratio) {
138         if (r != null && !r.isEmpty()) {
139             r.scale(ratio);
140         }
141     }
142 
scaleInsets(Insets insets, float ratio)143     private Insets scaleInsets(Insets insets, float ratio) {
144         return Insets.of(
145                 (int) (insets.left * ratio + 0.5f),
146                 (int) (insets.top * ratio + 0.5f),
147                 (int) (insets.right * ratio + 0.5f),
148                 (int) (insets.bottom * ratio + 0.5f));
149     }
150 
151     @VisibleForTesting(visibility = PACKAGE)
152     @Nullable
getPath()153     public Path getPath() {
154         return mPath;
155     }
156 
157     @VisibleForTesting(visibility = PACKAGE)
158     @Nullable
getLeftBound()159     public Rect getLeftBound() {
160         return mLeftBound;
161     }
162 
163     @VisibleForTesting(visibility = PACKAGE)
164     @Nullable
getTopBound()165     public Rect getTopBound() {
166         return mTopBound;
167     }
168 
169     @VisibleForTesting(visibility = PACKAGE)
170     @Nullable
getRightBound()171     public Rect getRightBound() {
172         return mRightBound;
173     }
174 
175     @VisibleForTesting(visibility = PACKAGE)
176     @Nullable
getBottomBound()177     public Rect getBottomBound() {
178         return mBottomBound;
179     }
180 
181     /**
182      * To count the safe inset according to the cutout bounds and waterfall inset.
183      *
184      * @return the safe inset.
185      */
186     @VisibleForTesting(visibility = PACKAGE)
187     @NonNull
getSafeInset()188     public Rect getSafeInset() {
189         return mInsets.toRect();
190     }
191 
decideWhichEdge(boolean isTopEdgeShortEdge, boolean isShortEdge, boolean isStart)192     private static int decideWhichEdge(boolean isTopEdgeShortEdge,
193             boolean isShortEdge, boolean isStart) {
194         return (isTopEdgeShortEdge)
195                 ? ((isShortEdge) ? (isStart ? TOP : BOTTOM) : (isStart ? LEFT : RIGHT))
196                 : ((isShortEdge) ? (isStart ? LEFT : RIGHT) : (isStart ? TOP : BOTTOM));
197     }
198 
199     /**
200      * The CutoutSpecification Parser.
201      */
202     @VisibleForTesting(visibility = PACKAGE)
203     public static class Parser {
204         private final boolean mIsShortEdgeOnTop;
205         private final float mStableDensity;
206         private final int mPhysicalDisplayWidth;
207         private final int mPhysicalDisplayHeight;
208         private final float mPhysicalPixelDisplaySizeRatio;
209         private final Matrix mMatrix;
210         private Insets mInsets;
211         private int mSafeInsetLeft;
212         private int mSafeInsetTop;
213         private int mSafeInsetRight;
214         private int mSafeInsetBottom;
215 
216         private final Rect mTmpRect = new Rect();
217         private final RectF mTmpRectF = new RectF();
218 
219         private boolean mInDp;
220 
221         private Path mPath;
222         private Rect mLeftBound;
223         private Rect mTopBound;
224         private Rect mRightBound;
225         private Rect mBottomBound;
226 
227         private boolean mPositionFromLeft = false;
228         private boolean mPositionFromRight = false;
229         private boolean mPositionFromBottom = false;
230         private boolean mPositionFromCenterVertical = false;
231 
232         private boolean mBindLeftCutout = false;
233         private boolean mBindRightCutout = false;
234         private boolean mBindBottomCutout = false;
235 
236         private boolean mIsTouchShortEdgeStart;
237         private boolean mIsTouchShortEdgeEnd;
238         private boolean mIsCloserToStartSide;
239 
240         @VisibleForTesting(visibility = PACKAGE)
Parser(float stableDensity, int physicalDisplayWidth, int physicalDisplayHeight)241         public Parser(float stableDensity, int physicalDisplayWidth,
242                 int physicalDisplayHeight) {
243             this(stableDensity, physicalDisplayWidth, physicalDisplayHeight, 1f);
244         }
245 
246         /**
247          * The constructor of the CutoutSpecification parser to parse the specification of cutout.
248          * @param stableDensity the display density.
249          * @param physicalDisplayWidth the display width.
250          * @param physicalDisplayHeight the display height.
251          * @param physicalPixelDisplaySizeRatio the display size ratio based on stable display size.
252          */
Parser(float stableDensity, int physicalDisplayWidth, int physicalDisplayHeight, float physicalPixelDisplaySizeRatio)253         Parser(float stableDensity, int physicalDisplayWidth, int physicalDisplayHeight,
254                 float physicalPixelDisplaySizeRatio) {
255             mStableDensity = stableDensity;
256             mPhysicalDisplayWidth = physicalDisplayWidth;
257             mPhysicalDisplayHeight = physicalDisplayHeight;
258             mPhysicalPixelDisplaySizeRatio = physicalPixelDisplaySizeRatio;
259             mMatrix = new Matrix();
260             mIsShortEdgeOnTop = mPhysicalDisplayWidth < mPhysicalDisplayHeight;
261         }
262 
263         private void computeBoundsRectAndAddToRegion(Path p, Region inoutRegion, Rect inoutRect) {
264             mTmpRectF.setEmpty();
265             p.computeBounds(mTmpRectF, false /* unused */);
266             mTmpRectF.round(inoutRect);
267             inoutRegion.op(inoutRect, Region.Op.UNION);
268         }
269 
270         private void resetStatus(StringBuilder sb) {
271             sb.setLength(0);
272             mPositionFromBottom = false;
273             mPositionFromLeft = false;
274             mPositionFromRight = false;
275             mPositionFromCenterVertical = false;
276 
277             mBindLeftCutout = false;
278             mBindRightCutout = false;
279             mBindBottomCutout = false;
280         }
281 
282         private void translateMatrix() {
283             final float offsetX;
284             if (mPositionFromRight) {
285                 offsetX = mPhysicalDisplayWidth;
286             } else if (mPositionFromLeft) {
287                 offsetX = 0;
288             } else {
289                 offsetX = mPhysicalDisplayWidth / 2f;
290             }
291 
292             final float offsetY;
293             if (mPositionFromBottom) {
294                 offsetY = mPhysicalDisplayHeight;
295             } else if (mPositionFromCenterVertical) {
296                 offsetY = mPhysicalDisplayHeight / 2f;
297             } else {
298                 offsetY = 0;
299             }
300 
301             mMatrix.reset();
302             if (mInDp) {
303                 mMatrix.postScale(mStableDensity, mStableDensity);
304             }
305             mMatrix.postTranslate(offsetX, offsetY);
306         }
307 
308         private int computeSafeInsets(int gravity, Rect rect) {
309             if (gravity == LEFT && rect.right > 0 && rect.right < mPhysicalDisplayWidth) {
310                 return rect.right;
311             } else if (gravity == TOP && rect.bottom > 0 && rect.bottom < mPhysicalDisplayHeight) {
312                 return rect.bottom;
313             } else if (gravity == RIGHT && rect.left > 0 && rect.left < mPhysicalDisplayWidth) {
314                 return mPhysicalDisplayWidth - rect.left;
315             } else if (gravity == BOTTOM && rect.top > 0 && rect.top < mPhysicalDisplayHeight) {
316                 return mPhysicalDisplayHeight - rect.top;
317             }
318             return 0;
319         }
320 
321         private void setSafeInset(int gravity, int inset) {
322             if (gravity == LEFT) {
323                 mSafeInsetLeft = inset;
324             } else if (gravity == TOP) {
325                 mSafeInsetTop = inset;
326             } else if (gravity == RIGHT) {
327                 mSafeInsetRight = inset;
328             } else if (gravity == BOTTOM) {
329                 mSafeInsetBottom = inset;
330             }
331         }
332 
333         private int getSafeInset(int gravity) {
334             if (gravity == LEFT) {
335                 return mSafeInsetLeft;
336             } else if (gravity == TOP) {
337                 return mSafeInsetTop;
338             } else if (gravity == RIGHT) {
339                 return mSafeInsetRight;
340             } else if (gravity == BOTTOM) {
341                 return mSafeInsetBottom;
342             }
343             return 0;
344         }
345 
346         @NonNull
347         private Rect onSetEdgeCutout(boolean isStart, boolean isShortEdge, @NonNull Rect rect) {
348             final int gravity;
349             if (isShortEdge) {
350                 gravity = decideWhichEdge(mIsShortEdgeOnTop, true, isStart);
351             } else {
352                 if (mIsTouchShortEdgeStart && mIsTouchShortEdgeEnd) {
353                     gravity = decideWhichEdge(mIsShortEdgeOnTop, false, isStart);
354                 } else if (mIsTouchShortEdgeStart || mIsTouchShortEdgeEnd) {
355                     gravity = decideWhichEdge(mIsShortEdgeOnTop, true,
356                             mIsCloserToStartSide);
357                 } else {
358                     gravity = decideWhichEdge(mIsShortEdgeOnTop, isShortEdge, isStart);
359                 }
360             }
361 
362             int oldSafeInset = getSafeInset(gravity);
363             int newSafeInset = computeSafeInsets(gravity, rect);
364             if (oldSafeInset < newSafeInset) {
365                 setSafeInset(gravity, newSafeInset);
366             }
367 
368             return new Rect(rect);
369         }
370 
371         private void setEdgeCutout(@NonNull Path newPath) {
372             if (mBindRightCutout && mRightBound == null) {
373                 mRightBound = onSetEdgeCutout(false, !mIsShortEdgeOnTop, mTmpRect);
374             } else if (mBindLeftCutout && mLeftBound == null) {
375                 mLeftBound = onSetEdgeCutout(true, !mIsShortEdgeOnTop, mTmpRect);
376             } else if (mBindBottomCutout && mBottomBound == null) {
377                 mBottomBound = onSetEdgeCutout(false, mIsShortEdgeOnTop, mTmpRect);
378             } else if (!(mBindBottomCutout || mBindLeftCutout || mBindRightCutout)
379                     && mTopBound == null) {
380                 mTopBound = onSetEdgeCutout(true, mIsShortEdgeOnTop, mTmpRect);
381             } else {
382                 return;
383             }
384 
385             if (mPath != null) {
386                 mPath.addPath(newPath);
387             } else {
388                 mPath = newPath;
389             }
390         }
391 
392         private void parseSvgPathSpec(Region region, String spec) {
393             if (TextUtils.length(spec) < MINIMAL_ACCEPTABLE_PATH_LENGTH) {
394                 Log.e(TAG, "According to SVG definition, it shouldn't happen");
395                 return;
396             }
397             spec.trim();
398             translateMatrix();
399 
400             final Path newPath = PathParser.createPathFromPathData(spec);
401             newPath.transform(mMatrix);
402             computeBoundsRectAndAddToRegion(newPath, region, mTmpRect);
403 
404             if (DEBUG) {
405                 Log.d(TAG, String.format(Locale.ENGLISH,
406                         "hasLeft = %b, hasRight = %b, hasBottom = %b, hasCenterVertical = %b",
407                         mPositionFromLeft, mPositionFromRight, mPositionFromBottom,
408                         mPositionFromCenterVertical));
409                 Log.d(TAG, "region = " + region);
410                 Log.d(TAG, "spec = \"" + spec + "\" rect = " + mTmpRect + " newPath = " + newPath);
411             }
412 
413             if (mTmpRect.isEmpty()) {
414                 return;
415             }
416 
417             if (mIsShortEdgeOnTop) {
418                 mIsTouchShortEdgeStart = mTmpRect.top <= 0;
419                 mIsTouchShortEdgeEnd = mTmpRect.bottom >= mPhysicalDisplayHeight;
420                 mIsCloserToStartSide = mTmpRect.centerY() < mPhysicalDisplayHeight / 2;
421             } else {
422                 mIsTouchShortEdgeStart = mTmpRect.left <= 0;
423                 mIsTouchShortEdgeEnd = mTmpRect.right >= mPhysicalDisplayWidth;
424                 mIsCloserToStartSide = mTmpRect.centerX() < mPhysicalDisplayWidth / 2;
425             }
426 
427             setEdgeCutout(newPath);
428         }
429 
430         private void parseSpecWithoutDp(@NonNull String specWithoutDp) {
431             Region region = Region.obtain();
432             StringBuilder sb = null;
433             int currentIndex = 0;
434             int lastIndex = 0;
435             while ((currentIndex = specWithoutDp.indexOf(MARKER_START_CHAR, lastIndex)) != -1) {
436                 if (sb == null) {
437                     sb = new StringBuilder(specWithoutDp.length());
438                 }
439                 sb.append(specWithoutDp, lastIndex, currentIndex);
440 
441                 if (specWithoutDp.startsWith(LEFT_MARKER, currentIndex)) {
442                     if (!mPositionFromRight) {
443                         mPositionFromLeft = true;
444                     }
445                     currentIndex += LEFT_MARKER.length();
446                 } else if (specWithoutDp.startsWith(RIGHT_MARKER, currentIndex)) {
447                     if (!mPositionFromLeft) {
448                         mPositionFromRight = true;
449                     }
450                     currentIndex += RIGHT_MARKER.length();
451                 } else if (specWithoutDp.startsWith(BOTTOM_MARKER, currentIndex)) {
452                     parseSvgPathSpec(region, sb.toString());
453                     currentIndex += BOTTOM_MARKER.length();
454 
455                     /* prepare to parse the rest path */
456                     resetStatus(sb);
457                     mBindBottomCutout = true;
458                     mPositionFromBottom = true;
459                 } else if (specWithoutDp.startsWith(CENTER_VERTICAL_MARKER, currentIndex)) {
460                     parseSvgPathSpec(region, sb.toString());
461                     currentIndex += CENTER_VERTICAL_MARKER.length();
462 
463                     /* prepare to parse the rest path */
464                     resetStatus(sb);
465                     mPositionFromCenterVertical = true;
466                 } else if (specWithoutDp.startsWith(CUTOUT_MARKER, currentIndex)) {
467                     parseSvgPathSpec(region, sb.toString());
468                     currentIndex += CUTOUT_MARKER.length();
469 
470                     /* prepare to parse the rest path */
471                     resetStatus(sb);
472                 } else if (specWithoutDp.startsWith(BIND_LEFT_CUTOUT_MARKER, currentIndex)) {
473                     mBindBottomCutout = false;
474                     mBindRightCutout = false;
475                     mBindLeftCutout = true;
476 
477                     currentIndex += BIND_LEFT_CUTOUT_MARKER.length();
478                 } else if (specWithoutDp.startsWith(BIND_RIGHT_CUTOUT_MARKER, currentIndex)) {
479                     mBindBottomCutout = false;
480                     mBindLeftCutout = false;
481                     mBindRightCutout = true;
482 
483                     currentIndex += BIND_RIGHT_CUTOUT_MARKER.length();
484                 } else {
485                     currentIndex += 1;
486                 }
487 
488                 lastIndex = currentIndex;
489             }
490 
491             if (sb == null) {
492                 parseSvgPathSpec(region, specWithoutDp);
493             } else {
494                 sb.append(specWithoutDp, lastIndex, specWithoutDp.length());
495                 parseSvgPathSpec(region, sb.toString());
496             }
497 
498             region.recycle();
499         }
500 
501         /**
502          * To parse specification string as the CutoutSpecification.
503          *
504          * @param originalSpec the specification string
505          * @return the CutoutSpecification instance
506          */
507         @VisibleForTesting(visibility = PACKAGE)
508         public CutoutSpecification parse(@NonNull String originalSpec) {
509             Objects.requireNonNull(originalSpec);
510 
511             int dpIndex = originalSpec.lastIndexOf(DP_MARKER);
512             mInDp = (dpIndex != -1);
513             final String spec;
514             if (dpIndex != -1) {
515                 spec = originalSpec.substring(0, dpIndex)
516                         + originalSpec.substring(dpIndex + DP_MARKER.length());
517             } else {
518                 spec = originalSpec;
519             }
520 
521             parseSpecWithoutDp(spec);
522             mInsets = Insets.of(mSafeInsetLeft, mSafeInsetTop, mSafeInsetRight, mSafeInsetBottom);
523             return new CutoutSpecification(this);
524         }
525     }
526 }
527