Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 489cf260 authored by Felka Chang's avatar Felka Chang
Browse files

To add new marker to support long edge cutout

Currently, the cutout only exists in short edge of a display. It means
that the cutout only locates in short edge of the display in portrait
mode. The method to parse the cutout only supports top boundary or bottom
boundary cutout.

To modify the parser of cutout specification supports the long edge cutout
feature. CutoutSpecification handles the parsing rulers extracted from
DisplayCutout.

In order to make parsing faster, it doesn't use regular expression to parse
specification and String.split.

Test: atest \
    FrameworksCoreTests:android.view.DisplayCutoutTest \
    FrameworksCoreTests:android.view.CutoutSpecificationTest \
    SystemUITests:com.android.systemui.ScreenDecorationsTest \
    CorePerfTests:android.view.CutoutSpecificationBenchmark

Bug: 146875639
Change-Id: Ice3ad28ef29a6f11875c4946cf4a60ee792f1270
parent 1f0cb0ac
Loading
Loading
Loading
Loading
+240 −0
Original line number Diff line number Diff line
/*
 * 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);
        }
    }
}
+486 −0

File added.

Preview size limit exceeded, changes collapsed.

+11 −90
Original line number Diff line number Diff line
@@ -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();
            }

            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),
+262 −0
Original line number Diff line number Diff line
/*
 * 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));
    }
}