summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apct-tests/perftests/core/src/android/view/CutoutSpecificationBenchmark.java240
-rw-r--r--core/java/android/view/CutoutSpecification.java486
-rw-r--r--core/java/android/view/DisplayCutout.java101
-rw-r--r--core/tests/coretests/src/android/view/CutoutSpecificationTest.java262
4 files changed, 999 insertions, 90 deletions
diff --git a/apct-tests/perftests/core/src/android/view/CutoutSpecificationBenchmark.java b/apct-tests/perftests/core/src/android/view/CutoutSpecificationBenchmark.java
new file mode 100644
index 000000000000..14282bfbd24e
--- /dev/null
+++ b/apct-tests/perftests/core/src/android/view/CutoutSpecificationBenchmark.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.text.TextUtils;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.PathParser;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class CutoutSpecificationBenchmark {
+ private static final String TAG = "CutoutSpecificationBenchmark";
+
+ private static final String BOTTOM_MARKER = "@bottom";
+ private static final String DP_MARKER = "@dp";
+ private static final String RIGHT_MARKER = "@right";
+ private static final String LEFT_MARKER = "@left";
+
+ private static final String DOUBLE_CUTOUT_SPEC = "M 0,0\n"
+ + "L -72, 0\n"
+ + "L -69.9940446283, 20.0595537175\n"
+ + "C -69.1582133885, 28.4178661152 -65.2, 32.0 -56.8, 32.0\n"
+ + "L 56.8, 32.0\n"
+ + "C 65.2, 32.0 69.1582133885, 28.4178661152 69.9940446283, 20.0595537175\n"
+ + "L 72, 0\n"
+ + "Z\n"
+ + "@bottom\n"
+ + "M 0,0\n"
+ + "L -72, 0\n"
+ + "L -69.9940446283, -20.0595537175\n"
+ + "C -69.1582133885, -28.4178661152 -65.2, -32.0 -56.8, -32.0\n"
+ + "L 56.8, -32.0\n"
+ + "C 65.2, -32.0 69.1582133885, -28.4178661152 69.9940446283, -20.0595537175\n"
+ + "L 72, 0\n"
+ + "Z\n"
+ + "@dp";
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ private Context mContext;
+ private DisplayMetrics mDisplayMetrics;
+
+ /**
+ * Setup the necessary member field used by test methods.
+ */
+ @Before
+ public void setUp() {
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+ mDisplayMetrics = new DisplayMetrics();
+ mContext.getDisplay().getRealMetrics(mDisplayMetrics);
+ }
+
+
+ private static void toRectAndAddToRegion(Path p, Region inoutRegion, Rect inoutRect) {
+ final RectF rectF = new RectF();
+ p.computeBounds(rectF, false /* unused */);
+ rectF.round(inoutRect);
+ inoutRegion.op(inoutRect, Region.Op.UNION);
+ }
+
+ private static void oldMethodParsingSpec(String spec, int displayWidth, int displayHeight,
+ float density) {
+ Path p = null;
+ Rect boundTop = null;
+ Rect boundBottom = null;
+ Rect safeInset = new Rect();
+ String bottomSpec = null;
+ if (!TextUtils.isEmpty(spec)) {
+ spec = spec.trim();
+ final float offsetX;
+ if (spec.endsWith(RIGHT_MARKER)) {
+ offsetX = displayWidth;
+ spec = spec.substring(0, spec.length() - RIGHT_MARKER.length()).trim();
+ } else if (spec.endsWith(LEFT_MARKER)) {
+ offsetX = 0;
+ spec = spec.substring(0, spec.length() - LEFT_MARKER.length()).trim();
+ } else {
+ offsetX = displayWidth / 2f;
+ }
+ final boolean inDp = spec.endsWith(DP_MARKER);
+ if (inDp) {
+ spec = spec.substring(0, spec.length() - DP_MARKER.length());
+ }
+
+ if (spec.contains(BOTTOM_MARKER)) {
+ String[] splits = spec.split(BOTTOM_MARKER, 2);
+ spec = splits[0].trim();
+ bottomSpec = splits[1].trim();
+ }
+
+ final Matrix m = new Matrix();
+ final Region r = Region.obtain();
+ if (!spec.isEmpty()) {
+ try {
+ p = PathParser.createPathFromPathData(spec);
+ } catch (Throwable e) {
+ Log.wtf(TAG, "Could not inflate cutout: ", e);
+ }
+
+ if (p != null) {
+ if (inDp) {
+ m.postScale(density, density);
+ }
+ m.postTranslate(offsetX, 0);
+ p.transform(m);
+
+ boundTop = new Rect();
+ toRectAndAddToRegion(p, r, boundTop);
+ safeInset.top = boundTop.bottom;
+ }
+ }
+
+ if (bottomSpec != null) {
+ int bottomInset = 0;
+ Path bottomPath = null;
+ try {
+ bottomPath = PathParser.createPathFromPathData(bottomSpec);
+ } catch (Throwable e) {
+ Log.wtf(TAG, "Could not inflate bottom cutout: ", e);
+ }
+
+ if (bottomPath != null) {
+ // Keep top transform
+ m.postTranslate(0, displayHeight);
+ bottomPath.transform(m);
+ p.addPath(bottomPath);
+ boundBottom = new Rect();
+ toRectAndAddToRegion(bottomPath, r, boundBottom);
+ bottomInset = displayHeight - boundBottom.top;
+ }
+ safeInset.bottom = bottomInset;
+ }
+ }
+ }
+
+ @Test
+ public void parseByOldMethodForDoubleCutout() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ oldMethodParsingSpec(DOUBLE_CUTOUT_SPEC, mDisplayMetrics.widthPixels,
+ mDisplayMetrics.heightPixels, mDisplayMetrics.density);
+ }
+ }
+
+ @Test
+ public void parseByNewMethodForDoubleCutout() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ new CutoutSpecification.Parser(mDisplayMetrics.density,
+ mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels)
+ .parse(DOUBLE_CUTOUT_SPEC);
+ }
+ }
+
+ @Test
+ public void parseLongEdgeCutout() {
+ final String spec = "M 0,0\n"
+ + "H 48\n"
+ + "V 48\n"
+ + "H -48\n"
+ + "Z\n"
+ + "@left\n"
+ + "@center_vertical\n"
+ + "M 0,0\n"
+ + "H 48\n"
+ + "V 48\n"
+ + "H -48\n"
+ + "Z\n"
+ + "@left\n"
+ + "@center_vertical\n"
+ + "M 0,0\n"
+ + "H -48\n"
+ + "V 48\n"
+ + "H 48\n"
+ + "Z\n"
+ + "@right\n"
+ + "@dp";
+
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ new CutoutSpecification.Parser(mDisplayMetrics.density,
+ mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels).parse(spec);
+ }
+ }
+
+ @Test
+ public void parseShortEdgeCutout() {
+ final String spec = "M 0,0\n"
+ + "H 48\n"
+ + "V 48\n"
+ + "H -48\n"
+ + "Z\n"
+ + "@bottom\n"
+ + "M 0,0\n"
+ + "H 48\n"
+ + "V -48\n"
+ + "H -48\n"
+ + "Z\n"
+ + "@dp";
+
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ new CutoutSpecification.Parser(mDisplayMetrics.density,
+ mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels).parse(spec);
+ }
+ }
+}
diff --git a/core/java/android/view/CutoutSpecification.java b/core/java/android/view/CutoutSpecification.java
new file mode 100644
index 000000000000..d21a9520e12c
--- /dev/null
+++ b/core/java/android/view/CutoutSpecification.java
@@ -0,0 +1,486 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.view.Gravity.BOTTOM;
+import static android.view.Gravity.LEFT;
+import static android.view.Gravity.RIGHT;
+import static android.view.Gravity.TOP;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Insets;
+import android.graphics.Matrix;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.PathParser;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Locale;
+import java.util.Objects;
+
+/**
+ * In order to accept the cutout specification for all of edges in devices, the specification
+ * parsing method is extracted from
+ * {@link android.view.DisplayCutout#fromResourcesRectApproximation(Resources, int, int)} to be
+ * the specified class for parsing the specification.
+ * BNF definition:
+ * <ul>
+ * <li>Cutouts Specification = ([Cutout Delimiter],Cutout Specification) {...}, [Dp] ; </li>
+ * <li>Cutout Specification = [Vertical Position], (SVG Path Element), [Horizontal Position]
+ * [Bind Cutout] ;</li>
+ * <li>Vertical Position = "@bottom" | "@center_vertical" ;</li>
+ * <li>Horizontal Position = "@left" | "@right" ;</li>
+ * <li>Bind Cutout = "@bind_left_cutout" | "@bind_right_cutout" ;</li>
+ * <li>Cutout Delimiter = "@cutout" ;</li>
+ * <li>Dp = "@dp"</li>
+ * </ul>
+ *
+ * <ul>
+ * <li>Vertical position is top by default if there is neither "@bottom" nor "@center_vertical"
+ * </li>
+ * <li>Horizontal position is center horizontal by default if there is neither "@left" nor
+ * "@right".</li>
+ * <li>@bottom make the cutout piece bind to bottom edge.</li>
+ * <li>both of @bind_left_cutout and @bind_right_cutout are use to claim the cutout belong to
+ * left or right edge cutout.</li>
+ * </ul>
+ *
+ * @hide
+ */
+@VisibleForTesting(visibility = PACKAGE)
+public class CutoutSpecification {
+ private static final String TAG = "CutoutSpecification";
+ private static final boolean DEBUG = false;
+
+ private static final int MINIMAL_ACCEPTABLE_PATH_LENGTH = "H1V1Z".length();
+
+ private static final char MARKER_START_CHAR = '@';
+ private static final String DP_MARKER = MARKER_START_CHAR + "dp";
+
+ private static final String BOTTOM_MARKER = MARKER_START_CHAR + "bottom";
+ private static final String RIGHT_MARKER = MARKER_START_CHAR + "right";
+ private static final String LEFT_MARKER = MARKER_START_CHAR + "left";
+ private static final String CUTOUT_MARKER = MARKER_START_CHAR + "cutout";
+ private static final String CENTER_VERTICAL_MARKER = MARKER_START_CHAR + "center_vertical";
+
+ /* By default, it's top bound cutout. That's why TOP_BOUND_CUTOUT_MARKER is not defined */
+ private static final String BIND_RIGHT_CUTOUT_MARKER = MARKER_START_CHAR + "bind_right_cutout";
+ private static final String BIND_LEFT_CUTOUT_MARKER = MARKER_START_CHAR + "bind_left_cutout";
+
+ private final Path mPath;
+ private final Rect mLeftBound;
+ private final Rect mTopBound;
+ private final Rect mRightBound;
+ private final Rect mBottomBound;
+ private final Insets mInsets;
+
+ private CutoutSpecification(@NonNull Parser parser) {
+ mPath = parser.mPath;
+ mLeftBound = parser.mLeftBound;
+ mTopBound = parser.mTopBound;
+ mRightBound = parser.mRightBound;
+ mBottomBound = parser.mBottomBound;
+ mInsets = parser.mInsets;
+
+ if (DEBUG) {
+ Log.d(TAG, String.format(Locale.ENGLISH,
+ "left cutout = %s, top cutout = %s, right cutout = %s, bottom cutout = %s",
+ mLeftBound != null ? mLeftBound.toString() : "",
+ mTopBound != null ? mTopBound.toString() : "",
+ mRightBound != null ? mRightBound.toString() : "",
+ mBottomBound != null ? mBottomBound.toString() : ""));
+ }
+ }
+
+ @VisibleForTesting(visibility = PACKAGE)
+ @Nullable
+ public Path getPath() {
+ return mPath;
+ }
+
+ @VisibleForTesting(visibility = PACKAGE)
+ @Nullable
+ public Rect getLeftBound() {
+ return mLeftBound;
+ }
+
+ @VisibleForTesting(visibility = PACKAGE)
+ @Nullable
+ public Rect getTopBound() {
+ return mTopBound;
+ }
+
+ @VisibleForTesting(visibility = PACKAGE)
+ @Nullable
+ public Rect getRightBound() {
+ return mRightBound;
+ }
+
+ @VisibleForTesting(visibility = PACKAGE)
+ @Nullable
+ public Rect getBottomBound() {
+ return mBottomBound;
+ }
+
+ /**
+ * To count the safe inset according to the cutout bounds and waterfall inset.
+ *
+ * @return the safe inset.
+ */
+ @VisibleForTesting(visibility = PACKAGE)
+ @NonNull
+ public Rect getSafeInset() {
+ return mInsets.toRect();
+ }
+
+ private static int decideWhichEdge(boolean isTopEdgeShortEdge,
+ boolean isShortEdge, boolean isStart) {
+ return (isTopEdgeShortEdge)
+ ? ((isShortEdge) ? (isStart ? TOP : BOTTOM) : (isStart ? LEFT : RIGHT))
+ : ((isShortEdge) ? (isStart ? LEFT : RIGHT) : (isStart ? TOP : BOTTOM));
+ }
+
+ /**
+ * The CutoutSpecification Parser.
+ */
+ @VisibleForTesting(visibility = PACKAGE)
+ public static class Parser {
+ private final boolean mIsShortEdgeOnTop;
+ private final float mDensity;
+ private final int mDisplayWidth;
+ private final int mDisplayHeight;
+ private final Matrix mMatrix;
+ private Insets mInsets;
+ private int mSafeInsetLeft;
+ private int mSafeInsetTop;
+ private int mSafeInsetRight;
+ private int mSafeInsetBottom;
+
+ private final Rect mTmpRect = new Rect();
+ private final RectF mTmpRectF = new RectF();
+
+ private boolean mInDp;
+
+ private Path mPath;
+ private Rect mLeftBound;
+ private Rect mTopBound;
+ private Rect mRightBound;
+ private Rect mBottomBound;
+
+ private boolean mPositionFromLeft = false;
+ private boolean mPositionFromRight = false;
+ private boolean mPositionFromBottom = false;
+ private boolean mPositionFromCenterVertical = false;
+
+ private boolean mBindLeftCutout = false;
+ private boolean mBindRightCutout = false;
+ private boolean mBindBottomCutout = false;
+
+ private boolean mIsTouchShortEdgeStart;
+ private boolean mIsTouchShortEdgeEnd;
+ private boolean mIsCloserToStartSide;
+
+ /**
+ * The constructor of the CutoutSpecification parser to parse the specification of cutout.
+ * @param density the display density.
+ * @param displayWidth the display width.
+ * @param displayHeight the display height.
+ */
+ @VisibleForTesting(visibility = PACKAGE)
+ public Parser(float density, int displayWidth, int displayHeight) {
+ mDensity = density;
+ mDisplayWidth = displayWidth;
+ mDisplayHeight = displayHeight;
+ mMatrix = new Matrix();
+ mIsShortEdgeOnTop = mDisplayWidth < mDisplayHeight;
+ }
+
+ private void computeBoundsRectAndAddToRegion(Path p, Region inoutRegion, Rect inoutRect) {
+ mTmpRectF.setEmpty();
+ p.computeBounds(mTmpRectF, false /* unused */);
+ mTmpRectF.round(inoutRect);
+ inoutRegion.op(inoutRect, Region.Op.UNION);
+ }
+
+ private void resetStatus(StringBuilder sb) {
+ sb.setLength(0);
+ mPositionFromBottom = false;
+ mPositionFromLeft = false;
+ mPositionFromRight = false;
+ mPositionFromCenterVertical = false;
+
+ mBindLeftCutout = false;
+ mBindRightCutout = false;
+ mBindBottomCutout = false;
+ }
+
+ private void translateMatrix() {
+ final float offsetX;
+ if (mPositionFromRight) {
+ offsetX = mDisplayWidth;
+ } else if (mPositionFromLeft) {
+ offsetX = 0;
+ } else {
+ offsetX = mDisplayWidth / 2f;
+ }
+
+ final float offsetY;
+ if (mPositionFromBottom) {
+ offsetY = mDisplayHeight;
+ } else if (mPositionFromCenterVertical) {
+ offsetY = mDisplayHeight / 2f;
+ } else {
+ offsetY = 0;
+ }
+
+ mMatrix.reset();
+ if (mInDp) {
+ mMatrix.postScale(mDensity, mDensity);
+ }
+ mMatrix.postTranslate(offsetX, offsetY);
+ }
+
+ private int computeSafeInsets(int gravity, Rect rect) {
+ if (gravity == LEFT && rect.right > 0 && rect.right < mDisplayWidth) {
+ return rect.right;
+ } else if (gravity == TOP && rect.bottom > 0 && rect.bottom < mDisplayHeight) {
+ return rect.bottom;
+ } else if (gravity == RIGHT && rect.left > 0 && rect.left < mDisplayWidth) {
+ return mDisplayWidth - rect.left;
+ } else if (gravity == BOTTOM && rect.top > 0 && rect.top < mDisplayHeight) {
+ return mDisplayHeight - rect.top;
+ }
+ return 0;
+ }
+
+ private void setSafeInset(int gravity, int inset) {
+ if (gravity == LEFT) {
+ mSafeInsetLeft = inset;
+ } else if (gravity == TOP) {
+ mSafeInsetTop = inset;
+ } else if (gravity == RIGHT) {
+ mSafeInsetRight = inset;
+ } else if (gravity == BOTTOM) {
+ mSafeInsetBottom = inset;
+ }
+ }
+
+ private int getSafeInset(int gravity) {
+ if (gravity == LEFT) {
+ return mSafeInsetLeft;
+ } else if (gravity == TOP) {
+ return mSafeInsetTop;
+ } else if (gravity == RIGHT) {
+ return mSafeInsetRight;
+ } else if (gravity == BOTTOM) {
+ return mSafeInsetBottom;
+ }
+ return 0;
+ }
+
+ @NonNull
+ private Rect onSetEdgeCutout(boolean isStart, boolean isShortEdge, @NonNull Rect rect) {
+ final int gravity;
+ if (isShortEdge) {
+ gravity = decideWhichEdge(mIsShortEdgeOnTop, true, isStart);
+ } else {
+ if (mIsTouchShortEdgeStart && mIsTouchShortEdgeEnd) {
+ gravity = decideWhichEdge(mIsShortEdgeOnTop, false, isStart);
+ } else if (mIsTouchShortEdgeStart || mIsTouchShortEdgeEnd) {
+ gravity = decideWhichEdge(mIsShortEdgeOnTop, true,
+ mIsCloserToStartSide);
+ } else {
+ gravity = decideWhichEdge(mIsShortEdgeOnTop, isShortEdge, isStart);
+ }
+ }
+
+ int oldSafeInset = getSafeInset(gravity);
+ int newSafeInset = computeSafeInsets(gravity, rect);
+ if (oldSafeInset < newSafeInset) {
+ setSafeInset(gravity, newSafeInset);
+ }
+
+ return new Rect(rect);
+ }
+
+ private void setEdgeCutout(@NonNull Path newPath) {
+ if (mBindRightCutout && mRightBound == null) {
+ mRightBound = onSetEdgeCutout(false, !mIsShortEdgeOnTop, mTmpRect);
+ } else if (mBindLeftCutout && mLeftBound == null) {
+ mLeftBound = onSetEdgeCutout(true, !mIsShortEdgeOnTop, mTmpRect);
+ } else if (mBindBottomCutout && mBottomBound == null) {
+ mBottomBound = onSetEdgeCutout(false, mIsShortEdgeOnTop, mTmpRect);
+ } else if (!(mBindBottomCutout || mBindLeftCutout || mBindRightCutout)
+ && mTopBound == null) {
+ mTopBound = onSetEdgeCutout(true, mIsShortEdgeOnTop, mTmpRect);
+ } else {
+ return;
+ }
+
+ if (mPath != null) {
+ mPath.addPath(newPath);
+ } else {
+ mPath = newPath;
+ }
+ }
+
+ private void parseSvgPathSpec(Region region, String spec) {
+ if (TextUtils.length(spec) < MINIMAL_ACCEPTABLE_PATH_LENGTH) {
+ Log.e(TAG, "According to SVG definition, it shouldn't happen");
+ return;
+ }
+ spec.trim();
+ translateMatrix();
+
+ final Path newPath = PathParser.createPathFromPathData(spec);
+ newPath.transform(mMatrix);
+ computeBoundsRectAndAddToRegion(newPath, region, mTmpRect);
+
+ if (DEBUG) {
+ Log.d(TAG, String.format(Locale.ENGLISH,
+ "hasLeft = %b, hasRight = %b, hasBottom = %b, hasCenterVertical = %b",
+ mPositionFromLeft, mPositionFromRight, mPositionFromBottom,
+ mPositionFromCenterVertical));
+ Log.d(TAG, "region = " + region);
+ Log.d(TAG, "spec = \"" + spec + "\" rect = " + mTmpRect + " newPath = " + newPath);
+ }
+
+ if (mTmpRect.isEmpty()) {
+ return;
+ }
+
+ if (mIsShortEdgeOnTop) {
+ mIsTouchShortEdgeStart = mTmpRect.top <= 0;
+ mIsTouchShortEdgeEnd = mTmpRect.bottom >= mDisplayHeight;
+ mIsCloserToStartSide = mTmpRect.centerY() < mDisplayHeight / 2;
+ } else {
+ mIsTouchShortEdgeStart = mTmpRect.left <= 0;
+ mIsTouchShortEdgeEnd = mTmpRect.right >= mDisplayWidth;
+ mIsCloserToStartSide = mTmpRect.centerX() < mDisplayWidth / 2;
+ }
+
+ setEdgeCutout(newPath);
+ }
+
+ private void parseSpecWithoutDp(@NonNull String specWithoutDp) {
+ Region region = Region.obtain();
+ StringBuilder sb = null;
+ int currentIndex = 0;
+ int lastIndex = 0;
+ while ((currentIndex = specWithoutDp.indexOf(MARKER_START_CHAR, lastIndex)) != -1) {
+ if (sb == null) {
+ sb = new StringBuilder(specWithoutDp.length());
+ }
+ sb.append(specWithoutDp, lastIndex, currentIndex);
+
+ if (specWithoutDp.startsWith(LEFT_MARKER, currentIndex)) {
+ if (!mPositionFromRight) {
+ mPositionFromLeft = true;
+ }
+ currentIndex += LEFT_MARKER.length();
+ } else if (specWithoutDp.startsWith(RIGHT_MARKER, currentIndex)) {
+ if (!mPositionFromLeft) {
+ mPositionFromRight = true;
+ }
+ currentIndex += RIGHT_MARKER.length();
+ } else if (specWithoutDp.startsWith(BOTTOM_MARKER, currentIndex)) {
+ if (!mPositionFromCenterVertical) {
+ parseSvgPathSpec(region, sb.toString());
+ }
+ currentIndex += BOTTOM_MARKER.length();
+
+ /* prepare to parse the rest path */
+ resetStatus(sb);
+ mBindBottomCutout = true;
+ mPositionFromBottom = true;
+ } else if (specWithoutDp.startsWith(CENTER_VERTICAL_MARKER, currentIndex)) {
+ if (!mPositionFromBottom) {
+ parseSvgPathSpec(region, sb.toString());
+ }
+ currentIndex += CENTER_VERTICAL_MARKER.length();
+
+ /* prepare to parse the rest path */
+ resetStatus(sb);
+ mPositionFromCenterVertical = true;
+ } else if (specWithoutDp.startsWith(CUTOUT_MARKER, currentIndex)) {
+ parseSvgPathSpec(region, sb.toString());
+ currentIndex += CUTOUT_MARKER.length();
+
+ /* prepare to parse the rest path */
+ resetStatus(sb);
+ } else if (specWithoutDp.startsWith(BIND_LEFT_CUTOUT_MARKER, currentIndex)) {
+ if (!mBindBottomCutout && !mBindRightCutout) {
+ mBindLeftCutout = true;
+ }
+ currentIndex += BIND_LEFT_CUTOUT_MARKER.length();
+ } else if (specWithoutDp.startsWith(BIND_RIGHT_CUTOUT_MARKER, currentIndex)) {
+ if (!mBindBottomCutout && !mBindLeftCutout) {
+ mBindRightCutout = true;
+ }
+ currentIndex += BIND_RIGHT_CUTOUT_MARKER.length();
+ } else {
+ currentIndex += 1;
+ }
+
+ lastIndex = currentIndex;
+ }
+
+ if (sb == null) {
+ parseSvgPathSpec(region, specWithoutDp);
+ } else {
+ sb.append(specWithoutDp, lastIndex, specWithoutDp.length());
+ parseSvgPathSpec(region, sb.toString());
+ }
+
+ region.recycle();
+ }
+
+ /**
+ * To parse specification string as the CutoutSpecification.
+ *
+ * @param originalSpec the specification string
+ * @return the CutoutSpecification instance
+ */
+ @VisibleForTesting(visibility = PACKAGE)
+ public CutoutSpecification parse(@NonNull String originalSpec) {
+ Objects.requireNonNull(originalSpec);
+
+ int dpIndex = originalSpec.lastIndexOf(DP_MARKER);
+ mInDp = (dpIndex != -1);
+ final String spec;
+ if (dpIndex != -1) {
+ spec = originalSpec.substring(0, dpIndex)
+ + originalSpec.substring(dpIndex + DP_MARKER.length());
+ } else {
+ spec = originalSpec;
+ }
+
+ parseSpecWithoutDp(spec);
+
+ mInsets = Insets.of(mSafeInsetLeft, mSafeInsetTop, mSafeInsetRight, mSafeInsetBottom);
+ return new CutoutSpecification(this);
+ }
+ }
+}
diff --git a/core/java/android/view/DisplayCutout.java b/core/java/android/view/DisplayCutout.java
index d433591f2b3c..31fc16188814 100644
--- a/core/java/android/view/DisplayCutout.java
+++ b/core/java/android/view/DisplayCutout.java
@@ -31,18 +31,12 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.res.Resources;
import android.graphics.Insets;
-import android.graphics.Matrix;
import android.graphics.Path;
import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.Region;
-import android.graphics.Region.Op;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
-import android.util.Log;
import android.util.Pair;
-import android.util.PathParser;
import android.util.proto.ProtoOutputStream;
import com.android.internal.R;
@@ -63,10 +57,6 @@ import java.util.List;
public final class DisplayCutout {
private static final String TAG = "DisplayCutout";
- private static final String BOTTOM_MARKER = "@bottom";
- private static final String DP_MARKER = "@dp";
- private static final String RIGHT_MARKER = "@right";
- private static final String LEFT_MARKER = "@left";
/**
* Category for overlays that allow emulating a display cutout on devices that don't have
@@ -703,77 +693,16 @@ public final class DisplayCutout {
}
}
- Path p = null;
- Rect boundTop = null;
- Rect boundBottom = null;
- Rect safeInset = new Rect();
- String bottomSpec = null;
- if (!TextUtils.isEmpty(spec)) {
- spec = spec.trim();
- final float offsetX;
- if (spec.endsWith(RIGHT_MARKER)) {
- offsetX = displayWidth;
- spec = spec.substring(0, spec.length() - RIGHT_MARKER.length()).trim();
- } else if (spec.endsWith(LEFT_MARKER)) {
- offsetX = 0;
- spec = spec.substring(0, spec.length() - LEFT_MARKER.length()).trim();
- } else {
- offsetX = displayWidth / 2f;
- }
- final boolean inDp = spec.endsWith(DP_MARKER);
- if (inDp) {
- spec = spec.substring(0, spec.length() - DP_MARKER.length());
- }
-
- if (spec.contains(BOTTOM_MARKER)) {
- String[] splits = spec.split(BOTTOM_MARKER, 2);
- spec = splits[0].trim();
- bottomSpec = splits[1].trim();
- }
+ spec = spec.trim();
- final Matrix m = new Matrix();
- final Region r = Region.obtain();
- if (!spec.isEmpty()) {
- try {
- p = PathParser.createPathFromPathData(spec);
- } catch (Throwable e) {
- Log.wtf(TAG, "Could not inflate cutout: ", e);
- }
-
- if (p != null) {
- if (inDp) {
- m.postScale(density, density);
- }
- m.postTranslate(offsetX, 0);
- p.transform(m);
+ CutoutSpecification cutoutSpec = new CutoutSpecification.Parser(density,
+ displayWidth, displayHeight).parse(spec);
+ Rect safeInset = cutoutSpec.getSafeInset();
+ final Rect boundLeft = cutoutSpec.getLeftBound();
+ final Rect boundTop = cutoutSpec.getTopBound();
+ final Rect boundRight = cutoutSpec.getRightBound();
+ final Rect boundBottom = cutoutSpec.getBottomBound();
- boundTop = new Rect();
- toRectAndAddToRegion(p, r, boundTop);
- safeInset.top = boundTop.bottom;
- }
- }
-
- if (bottomSpec != null) {
- int bottomInset = 0;
- Path bottomPath = null;
- try {
- bottomPath = PathParser.createPathFromPathData(bottomSpec);
- } catch (Throwable e) {
- Log.wtf(TAG, "Could not inflate bottom cutout: ", e);
- }
-
- if (bottomPath != null) {
- // Keep top transform
- m.postTranslate(0, displayHeight);
- bottomPath.transform(m);
- p.addPath(bottomPath);
- boundBottom = new Rect();
- toRectAndAddToRegion(bottomPath, r, boundBottom);
- bottomInset = displayHeight - boundBottom.top;
- }
- safeInset.bottom = bottomInset;
- }
- }
if (!waterfallInsets.equals(Insets.NONE)) {
safeInset.set(
@@ -784,9 +713,9 @@ public final class DisplayCutout {
}
final DisplayCutout cutout = new DisplayCutout(
- safeInset, waterfallInsets, null /* boundLeft */, boundTop,
- null /* boundRight */, boundBottom, false /* copyArguments */);
- final Pair<Path, DisplayCutout> result = new Pair<>(p, cutout);
+ safeInset, waterfallInsets, boundLeft, boundTop,
+ boundRight, boundBottom, false /* copyArguments */);
+ final Pair<Path, DisplayCutout> result = new Pair<>(cutoutSpec.getPath(), cutout);
synchronized (CACHE_LOCK) {
sCachedSpec = spec;
sCachedDisplayWidth = displayWidth;
@@ -798,14 +727,6 @@ public final class DisplayCutout {
return result;
}
- private static void toRectAndAddToRegion(Path p, Region inoutRegion, Rect inoutRect) {
- final RectF rectF = new RectF();
- p.computeBounds(rectF, false /* unused */);
- rectF.round(inoutRect);
- inoutRegion.op(inoutRect, Op.UNION);
- }
-
-
private static Insets loadWaterfallInset(Resources res) {
return Insets.of(
res.getDimensionPixelSize(R.dimen.waterfall_display_left_edge_size),
diff --git a/core/tests/coretests/src/android/view/CutoutSpecificationTest.java b/core/tests/coretests/src/android/view/CutoutSpecificationTest.java
new file mode 100644
index 000000000000..1f831bb2f9a8
--- /dev/null
+++ b/core/tests/coretests/src/android/view/CutoutSpecificationTest.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.graphics.Rect;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class CutoutSpecificationTest {
+ private static final String WITHOUT_BIND_CUTOUT_SPECIFICATION = "M 0,0\n"
+ + "h 48\n"
+ + "v 48\n"
+ + "h -48\n"
+ + "z\n"
+ + "@left\n"
+ + "@center_vertical\n"
+ + "M 0,0\n"
+ + "h 48\n"
+ + "v 48\n"
+ + "h -48\n"
+ + "z\n"
+ + "@left\n"
+ + "@center_vertical\n"
+ + "M 0,0\n"
+ + "h -48\n"
+ + "v 48\n"
+ + "h 48\n"
+ + "z\n"
+ + "@right\n"
+ + "@dp";
+ private static final String WITH_BIND_CUTOUT_SPECIFICATION = "M 0,0\n"
+ + "h 48\n"
+ + "v 48\n"
+ + "h -48\n"
+ + "z\n"
+ + "@left\n"
+ + "@center_vertical\n"
+ + "M 0,0\n"
+ + "h 48\n"
+ + "v 48\n"
+ + "h -48\n"
+ + "z\n"
+ + "@left\n"
+ + "@bind_left_cutout\n"
+ + "@center_vertical\n"
+ + "M 0,0\n"
+ + "h -48\n"
+ + "v 48\n"
+ + "h 48\n"
+ + "z\n"
+ + "@right\n"
+ + "@bind_right_cutout\n"
+ + "@dp";
+ private static final String CORNER_CUTOUT_SPECIFICATION = "M 0,0\n"
+ + "h 1\n"
+ + "v 1\n"
+ + "h -1\n"
+ + "z\n"
+ + "@left\n"
+ + "@cutout\n"
+ + "M 0, 0\n"
+ + "h -2\n"
+ + "v 2\n"
+ + "h 2\n"
+ + "z\n"
+ + "@right\n"
+ + "@bind_right_cutout\n"
+ + "@cutout\n"
+ + "M 0, 200\n"
+ + "h 3\n"
+ + "v -3\n"
+ + "h -3\n"
+ + "z\n"
+ + "@left\n"
+ + "@bind_left_cutout\n"
+ + "@bottom\n"
+ + "M 0, 0\n"
+ + "h -4\n"
+ + "v -4\n"
+ + "h 4\n"
+ + "z\n"
+ + "@right\n"
+ + "@dp";
+
+ private CutoutSpecification.Parser mParser;
+
+ /**
+ * Setup the necessary member field used by test methods.
+ */
+ @Before
+ public void setUp() {
+ mParser = new CutoutSpecification.Parser(3.5f, 1080, 1920);
+ }
+
+ @Test
+ public void parse_nullString_shouldTriggerException() {
+ assertThrows(NullPointerException.class, () -> mParser.parse(null));
+ }
+
+ @Test
+ public void parse_emptyString_pathShouldBeNull() {
+ CutoutSpecification cutoutSpecification = mParser.parse("");
+ assertThat(cutoutSpecification.getPath()).isNull();
+ }
+
+ @Test
+ public void parse_withoutBindMarker_shouldHaveNoLeftBound() {
+ CutoutSpecification cutoutSpecification = mParser.parse(WITHOUT_BIND_CUTOUT_SPECIFICATION);
+ assertThat(cutoutSpecification.getLeftBound()).isNull();
+ }
+
+ @Test
+ public void parse_withoutBindMarker_shouldHaveNoRightBound() {
+ CutoutSpecification cutoutSpecification = mParser.parse(WITHOUT_BIND_CUTOUT_SPECIFICATION);
+ assertThat(cutoutSpecification.getRightBound()).isNull();
+ }
+
+ @Test
+ public void parse_withBindMarker_shouldHaveLeftBound() {
+ CutoutSpecification cutoutSpecification = mParser.parse(WITH_BIND_CUTOUT_SPECIFICATION);
+ assertThat(cutoutSpecification.getLeftBound()).isEqualTo(new Rect(0, 960, 168, 1128));
+ }
+
+ @Test
+ public void parse_withBindMarker_shouldHaveRightBound() {
+ CutoutSpecification cutoutSpecification = mParser.parse(WITH_BIND_CUTOUT_SPECIFICATION);
+ assertThat(cutoutSpecification.getRightBound()).isEqualTo(new Rect(912, 960, 1080, 1128));
+ }
+
+ @Test
+ public void parse_tallCutout_shouldBeDone() {
+ CutoutSpecification cutoutSpecification = mParser.parse("M 0,0\n"
+ + "L -48, 0\n"
+ + "L -44.3940446283, 36.0595537175\n"
+ + "C -43.5582133885, 44.4178661152 -39.6, 48.0 -31.2, 48.0\n"
+ + "L 31.2, 48.0\n"
+ + "C 39.6, 48.0 43.5582133885, 44.4178661152 44.3940446283, 36.0595537175\n"
+ + "L 48, 0\n"
+ + "Z\n"
+ + "@dp");
+
+ assertThat(cutoutSpecification.getTopBound().height()).isEqualTo(168);
+ }
+
+ @Test
+ public void parse_wideCutout_shouldBeDone() {
+ CutoutSpecification cutoutSpecification = mParser.parse("M 0,0\n"
+ + "L -72, 0\n"
+ + "L -69.9940446283, 20.0595537175\n"
+ + "C -69.1582133885, 28.4178661152 -65.2, 32.0 -56.8, 32.0\n"
+ + "L 56.8, 32.0\n"
+ + "C 65.2, 32.0 69.1582133885, 28.4178661152 69.9940446283, 20.0595537175\n"
+ + "L 72, 0\n"
+ + "Z\n"
+ + "@dp");
+
+ assertThat(cutoutSpecification.getTopBound().width()).isEqualTo(504);
+ }
+
+ @Test
+ public void parse_narrowCutout_shouldBeDone() {
+ CutoutSpecification cutoutSpecification = mParser.parse("M 0,0\n"
+ + "L -24, 0\n"
+ + "L -21.9940446283, 20.0595537175\n"
+ + "C -21.1582133885, 28.4178661152 -17.2, 32.0 -8.8, 32.0\n"
+ + "L 8.8, 32.0\n"
+ + "C 17.2, 32.0 21.1582133885, 28.4178661152 21.9940446283, 20.0595537175\n"
+ + "L 24, 0\n"
+ + "Z\n"
+ + "@dp");
+
+ assertThat(cutoutSpecification.getTopBound().width()).isEqualTo(168);
+ }
+
+ @Test
+ public void parse_doubleCutout_shouldBeDone() {
+ CutoutSpecification cutoutSpecification = mParser.parse("M 0,0\n"
+ + "L -72, 0\n"
+ + "L -69.9940446283, 20.0595537175\n"
+ + "C -69.1582133885, 28.4178661152 -65.2, 32.0 -56.8, 32.0\n"
+ + "L 56.8, 32.0\n"
+ + "C 65.2, 32.0 69.1582133885, 28.4178661152 69.9940446283, 20.0595537175\n"
+ + "L 72, 0\n"
+ + "Z\n"
+ + "@bottom\n"
+ + "M 0,0\n"
+ + "L -72, 0\n"
+ + "L -69.9940446283, -20.0595537175\n"
+ + "C -69.1582133885, -28.4178661152 -65.2, -32.0 -56.8, -32.0\n"
+ + "L 56.8, -32.0\n"
+ + "C 65.2, -32.0 69.1582133885, -28.4178661152 69.9940446283, -20"
+ + ".0595537175\n"
+ + "L 72, 0\n"
+ + "Z\n"
+ + "@dp");
+
+ assertThat(cutoutSpecification.getTopBound().height()).isEqualTo(112);
+ }
+
+ @Test
+ public void parse_cornerCutout_shouldBeDone() {
+ CutoutSpecification cutoutSpecification = mParser.parse("M 0,0\n"
+ + "L -48, 0\n"
+ + "C -48,48 -48,48 0,48\n"
+ + "Z\n"
+ + "@dp\n"
+ + "@right");
+
+ assertThat(cutoutSpecification.getTopBound().height()).isEqualTo(168);
+ }
+
+ @Test
+ public void parse_holeCutout_shouldBeDone() {
+ CutoutSpecification cutoutSpecification = mParser.parse("M 20.0,20.0\n"
+ + "h 136\n"
+ + "v 136\n"
+ + "h -136\n"
+ + "Z\n"
+ + "@left");
+
+ assertThat(cutoutSpecification.getSafeInset()).isEqualTo(new Rect(0, 156, 0, 0));
+ }
+
+ @Test
+ public void getSafeInset_shortEdgeIsTopBottom_shouldMatchExpectedInset() {
+ CutoutSpecification cutoutSpecification =
+ new CutoutSpecification.Parser(2f, 200, 400)
+ .parse(CORNER_CUTOUT_SPECIFICATION);
+
+ assertThat(cutoutSpecification.getSafeInset())
+ .isEqualTo(new Rect(0, 4, 0, 8));
+ }
+
+ @Test
+ public void getSafeInset_shortEdgeIsLeftRight_shouldMatchExpectedInset() {
+ CutoutSpecification cutoutSpecification =
+ new CutoutSpecification.Parser(2f, 400, 200)
+ .parse(CORNER_CUTOUT_SPECIFICATION);
+
+ assertThat(cutoutSpecification.getSafeInset())
+ .isEqualTo(new Rect(6, 0, 8, 0));
+ }
+}