From 8c28c7c2d91fde8bac708e15bb12e53d0693e5c9 Mon Sep 17 00:00:00 2001 From: Adrian Roos Date: Mon, 20 Aug 2018 13:43:38 +0200 Subject: [PATCH] Cutout: Add developer setting to mask the display cutout Adds an option to mask the cutout by effectively shrinking the logical display such that developers can test apps as if the device did not have a notch. Bug: 112876936 Test: Go to Settings > Developer Options > Simulate display with cutout > "No cutout". Cutout should be hidden. Rotate screen, take screenshots, screenrecord, screen off animation should all work as expected. Change-Id: I5cdb201734d238bf3785ab55843114e4b5b4ee41 --- core/java/android/view/DisplayCutout.java | 24 +++++++--- core/res/res/values/config.xml | 4 ++ core/res/res/values/dimens.xml | 9 ++++ core/res/res/values/symbols.xml | 2 + .../src/android/view/DisplayCutoutTest.java | 14 ++++++ packages/SystemUI/res/values/dimens.xml | 6 +-- .../server/display/DisplayDeviceInfo.java | 9 ++++ .../server/display/LocalDisplayAdapter.java | 4 ++ .../server/display/LogicalDisplay.java | 47 +++++++++++++++---- .../android/server/wm/utils/InsetUtils.java | 27 +++++++++++ .../server/wm/utils/InsetUtilsTest.java | 29 ++++++++++++ 11 files changed, 156 insertions(+), 19 deletions(-) diff --git a/core/java/android/view/DisplayCutout.java b/core/java/android/view/DisplayCutout.java index 496bc9ff5383b..5f80d31651a8c 100644 --- a/core/java/android/view/DisplayCutout.java +++ b/core/java/android/view/DisplayCutout.java @@ -325,6 +325,7 @@ public final class DisplayCutout { * * @hide */ + @VisibleForTesting public static DisplayCutout fromBoundingRect(int left, int top, int right, int bottom) { Region r = Region.obtain(); r.set(left, top, right, bottom); @@ -422,8 +423,11 @@ public final class DisplayCutout { m.postTranslate(offsetX, 0); p.transform(m); - addToRegion(p, r); + final Rect tmpRect = new Rect(); + toRectAndAddToRegion(p, r, tmpRect); + final int topInset = tmpRect.bottom; + final int bottomInset; if (bottomSpec != null) { final Path bottomPath; try { @@ -436,10 +440,17 @@ public final class DisplayCutout { m.postTranslate(0, displayHeight); bottomPath.transform(m); p.addPath(bottomPath); - addToRegion(bottomPath, r); + toRectAndAddToRegion(bottomPath, r, tmpRect); + bottomInset = displayHeight - tmpRect.top; + } else { + bottomInset = 0; } - final Pair result = new Pair<>(p, fromBounds(r)); + // Reuse tmpRect as the inset rect we store into the DisplayCutout instance. + tmpRect.set(0, topInset, 0, bottomInset); + final DisplayCutout cutout = new DisplayCutout(tmpRect, r, false /* copyArguments */); + + final Pair result = new Pair<>(p, cutout); synchronized (CACHE_LOCK) { sCachedSpec = spec; sCachedDisplayWidth = displayWidth; @@ -450,12 +461,11 @@ public final class DisplayCutout { return result; } - private static void addToRegion(Path p, Region r) { + private static void toRectAndAddToRegion(Path p, Region inoutRegion, Rect inoutRect) { final RectF rectF = new RectF(); - final Rect rect = new Rect(); p.computeBounds(rectF, false /* unused */); - rectF.round(rect); - r.op(rect, Op.UNION); + rectF.round(inoutRect); + inoutRegion.op(inoutRect, Op.UNION); } private static Region boundingRectsToRegion(List rects) { diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 8f56ef951d8f8..cfee43626cdd9 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2981,6 +2981,10 @@ --> false + + false + diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 471170bbe93bb..73cb59e846442 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -61,6 +61,15 @@ 5dp + + 0dp + + 0dp + + 0dp + 48dp diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 00bd77d329355..9afeca6557bb4 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3407,6 +3407,8 @@ + + diff --git a/core/tests/coretests/src/android/view/DisplayCutoutTest.java b/core/tests/coretests/src/android/view/DisplayCutoutTest.java index 6ee74cb9a7428..fe45fe7d3aaf1 100644 --- a/core/tests/coretests/src/android/view/DisplayCutoutTest.java +++ b/core/tests/coretests/src/android/view/DisplayCutoutTest.java @@ -19,6 +19,7 @@ package android.view; import static android.view.DisplayCutout.NO_CUTOUT; import static android.view.DisplayCutout.fromSpec; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.sameInstance; import static org.junit.Assert.assertEquals; @@ -219,6 +220,19 @@ public class DisplayCutoutTest { assertThat(fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 1f), not(sameInstance(cached))); } + @Test + public void fromSpec_setsSafeInsets_top() { + DisplayCutout cutout = fromSpec("M -50,0 v 20 h 100 v -20 z", 200, 400, 2f); + assertThat(cutout.getSafeInsets(), equalTo(new Rect(0, 20, 0, 0))); + } + + @Test + public void fromSpec_setsSafeInsets_top_and_bottom() { + DisplayCutout cutout = fromSpec("M -50,0 v 20 h 100 v -20 z" + + "@bottom M -50,0 v -10,0 h 100 v 20 z", 200, 400, 2f); + assertThat(cutout.getSafeInsets(), equalTo(new Rect(0, 20, 0, 10))); + } + @Test public void parcel_unparcel_nocutout() { Parcel p = Parcel.obtain(); diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 3c84e5a910264..7d90e02d0f2f6 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -939,9 +939,9 @@ 48dp 8dp - 0dp - 0dp - 0dp + @*android:dimen/rounded_corner_radius + @*android:dimen/rounded_corner_radius_top + @*android:dimen/rounded_corner_radius_bottom 0dp 0dp 24dp diff --git a/services/core/java/com/android/server/display/DisplayDeviceInfo.java b/services/core/java/com/android/server/display/DisplayDeviceInfo.java index 349e1c8b713e3..512e85192d36f 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceInfo.java +++ b/services/core/java/com/android/server/display/DisplayDeviceInfo.java @@ -103,6 +103,12 @@ final class DisplayDeviceInfo { */ public static final int FLAG_DESTROY_CONTENT_ON_REMOVAL = 1 << 10; + /** + * Flag: The display cutout of this display is masked. + * @hide + */ + public static final int FLAG_MASK_DISPLAY_CUTOUT = 1 << 11; + /** * Touch attachment: Display does not receive touch. */ @@ -453,6 +459,9 @@ final class DisplayDeviceInfo { if ((flags & FLAG_CAN_SHOW_WITH_INSECURE_KEYGUARD) != 0) { msg.append(", FLAG_CAN_SHOW_WITH_INSECURE_KEYGUARD"); } + if ((flags & FLAG_MASK_DISPLAY_CUTOUT) != 0) { + msg.append(", FLAG_MASK_DISPLAY_CUTOUT"); + } return msg.toString(); } } diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java index 21ae048e1d756..16d82df4dd5ba 100644 --- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java +++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java @@ -402,6 +402,10 @@ final class LocalDisplayAdapter extends DisplayAdapter { && SystemProperties.getBoolean(PROPERTY_EMULATOR_CIRCULAR, false))) { mInfo.flags |= DisplayDeviceInfo.FLAG_ROUND; } + if (res.getBoolean( + com.android.internal.R.bool.config_maskMainBuiltInDisplayCutout)) { + mInfo.flags |= DisplayDeviceInfo.FLAG_MASK_DISPLAY_CUTOUT; + } mInfo.displayCutout = DisplayCutout.fromResourcesRectApproximation(res, mInfo.width, mInfo.height); mInfo.type = Display.TYPE_BUILT_IN; diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java index 23ee56b24b19d..373de63c0ec97 100644 --- a/services/core/java/com/android/server/display/LogicalDisplay.java +++ b/services/core/java/com/android/server/display/LogicalDisplay.java @@ -23,6 +23,8 @@ import android.view.DisplayInfo; import android.view.Surface; import android.view.SurfaceControl; +import com.android.server.wm.utils.InsetUtils; + import java.io.PrintWriter; import java.util.Arrays; import java.util.List; @@ -251,14 +253,18 @@ final class LogicalDisplay { if ((deviceInfo.flags & DisplayDeviceInfo.FLAG_CAN_SHOW_WITH_INSECURE_KEYGUARD) != 0) { mBaseDisplayInfo.flags |= Display.FLAG_CAN_SHOW_WITH_INSECURE_KEYGUARD; } + Rect maskingInsets = getMaskingInsets(deviceInfo); + int maskedWidth = deviceInfo.width - maskingInsets.left - maskingInsets.right; + int maskedHeight = deviceInfo.height - maskingInsets.top - maskingInsets.bottom; + mBaseDisplayInfo.type = deviceInfo.type; mBaseDisplayInfo.address = deviceInfo.address; mBaseDisplayInfo.name = deviceInfo.name; mBaseDisplayInfo.uniqueId = deviceInfo.uniqueId; - mBaseDisplayInfo.appWidth = deviceInfo.width; - mBaseDisplayInfo.appHeight = deviceInfo.height; - mBaseDisplayInfo.logicalWidth = deviceInfo.width; - mBaseDisplayInfo.logicalHeight = deviceInfo.height; + mBaseDisplayInfo.appWidth = maskedWidth; + mBaseDisplayInfo.appHeight = maskedHeight; + mBaseDisplayInfo.logicalWidth = maskedWidth; + mBaseDisplayInfo.logicalHeight = maskedHeight; mBaseDisplayInfo.rotation = Surface.ROTATION_0; mBaseDisplayInfo.modeId = deviceInfo.modeId; mBaseDisplayInfo.defaultModeId = deviceInfo.defaultModeId; @@ -275,19 +281,33 @@ final class LogicalDisplay { mBaseDisplayInfo.appVsyncOffsetNanos = deviceInfo.appVsyncOffsetNanos; mBaseDisplayInfo.presentationDeadlineNanos = deviceInfo.presentationDeadlineNanos; mBaseDisplayInfo.state = deviceInfo.state; - mBaseDisplayInfo.smallestNominalAppWidth = deviceInfo.width; - mBaseDisplayInfo.smallestNominalAppHeight = deviceInfo.height; - mBaseDisplayInfo.largestNominalAppWidth = deviceInfo.width; - mBaseDisplayInfo.largestNominalAppHeight = deviceInfo.height; + mBaseDisplayInfo.smallestNominalAppWidth = maskedWidth; + mBaseDisplayInfo.smallestNominalAppHeight = maskedHeight; + mBaseDisplayInfo.largestNominalAppWidth = maskedWidth; + mBaseDisplayInfo.largestNominalAppHeight = maskedHeight; mBaseDisplayInfo.ownerUid = deviceInfo.ownerUid; mBaseDisplayInfo.ownerPackageName = deviceInfo.ownerPackageName; - mBaseDisplayInfo.displayCutout = deviceInfo.displayCutout; + boolean maskCutout = + (deviceInfo.flags & DisplayDeviceInfo.FLAG_MASK_DISPLAY_CUTOUT) != 0; + mBaseDisplayInfo.displayCutout = maskCutout ? null : deviceInfo.displayCutout; mPrimaryDisplayDeviceInfo = deviceInfo; mInfo = null; } } + /** + * Returns insets in ROTATION_0 for areas that are masked. + */ + private static Rect getMaskingInsets(DisplayDeviceInfo deviceInfo) { + boolean maskCutout = (deviceInfo.flags & DisplayDeviceInfo.FLAG_MASK_DISPLAY_CUTOUT) != 0; + if (maskCutout && deviceInfo.displayCutout != null) { + return deviceInfo.displayCutout.getSafeInsets(); + } else { + return new Rect(); + } + } + /** * Applies the layer stack and transformation to the given display device * so that it shows the contents of this logical display. @@ -349,6 +369,12 @@ final class LogicalDisplay { int physWidth = rotated ? displayDeviceInfo.height : displayDeviceInfo.width; int physHeight = rotated ? displayDeviceInfo.width : displayDeviceInfo.height; + Rect maskingInsets = getMaskingInsets(displayDeviceInfo); + InsetUtils.rotateInsets(maskingInsets, orientation); + // Don't consider the masked area as available when calculating the scaling below. + physWidth -= maskingInsets.left + maskingInsets.right; + physHeight -= maskingInsets.top + maskingInsets.bottom; + // Determine whether the width or height is more constrained to be scaled. // physWidth / displayInfo.logicalWidth => letter box // or physHeight / displayInfo.logicalHeight => pillar box @@ -375,6 +401,9 @@ final class LogicalDisplay { mTempDisplayRect.set(displayRectLeft, displayRectTop, displayRectLeft + displayRectWidth, displayRectTop + displayRectHeight); + // Now add back the offset for the masked area. + mTempDisplayRect.offset(maskingInsets.left, maskingInsets.top); + mTempDisplayRect.left += mDisplayOffsetX; mTempDisplayRect.right += mDisplayOffsetX; mTempDisplayRect.top += mDisplayOffsetY; diff --git a/services/core/java/com/android/server/wm/utils/InsetUtils.java b/services/core/java/com/android/server/wm/utils/InsetUtils.java index b4a998add374f..c8600dd151d2c 100644 --- a/services/core/java/com/android/server/wm/utils/InsetUtils.java +++ b/services/core/java/com/android/server/wm/utils/InsetUtils.java @@ -17,6 +17,7 @@ package com.android.server.wm.utils; import android.graphics.Rect; +import android.view.Surface; /** * Utility methods to handle insets represented as rects. @@ -26,6 +27,32 @@ public class InsetUtils { private InsetUtils() { } + /** + * Transforms insets given in one rotation into insets in a different rotation. + * + * @param inOutInsets the insets to transform, is set to the transformed insets + * @param rotationDelta the delta between the new and old rotation. + * Must be one of Surface.ROTATION_0/90/180/270. + */ + public static void rotateInsets(Rect inOutInsets, int rotationDelta) { + final Rect r = inOutInsets; + switch (rotationDelta) { + case Surface.ROTATION_0: + return; + case Surface.ROTATION_90: + r.set(r.top, r.right, r.bottom, r.left); + break; + case Surface.ROTATION_180: + r.set(r.right, r.bottom, r.left, r.top); + break; + case Surface.ROTATION_270: + r.set(r.bottom, r.left, r.top, r.right); + break; + default: + throw new IllegalArgumentException("Unknown rotation: " + rotationDelta); + } + } + /** * Adds {@code insetsToAdd} to {@code inOutInsets}. */ diff --git a/services/tests/servicestests/src/com/android/server/wm/utils/InsetUtilsTest.java b/services/tests/servicestests/src/com/android/server/wm/utils/InsetUtilsTest.java index d0f0fe315bcfd..08bcc3d751f2e 100644 --- a/services/tests/servicestests/src/com/android/server/wm/utils/InsetUtilsTest.java +++ b/services/tests/servicestests/src/com/android/server/wm/utils/InsetUtilsTest.java @@ -16,6 +16,11 @@ package com.android.server.wm.utils; +import static android.hardware.camera2.params.OutputConfiguration.ROTATION_90; +import static android.view.Surface.ROTATION_0; +import static android.view.Surface.ROTATION_180; +import static android.view.Surface.ROTATION_270; + import static junit.framework.Assert.assertEquals; import android.graphics.Rect; @@ -39,5 +44,29 @@ public class InsetUtilsTest { InsetUtils.addInsets(rect1, rect2); assertEquals(new Rect(60, 80, 100, 120), rect1); } + + @Test + public void rotate() { + final Rect original = new Rect(1, 2, 3, 4); + + assertEquals("rot0", original, rotateCopy(original, ROTATION_0)); + + final Rect rot90 = rotateCopy(original, ROTATION_90); + assertEquals("rot90", new Rect(2, 3, 4, 1), rot90); + + final Rect rot180 = rotateCopy(original, ROTATION_180); + assertEquals("rot180", new Rect(3, 4, 1, 2), rot180); + assertEquals("rot90(rot90)=rot180", rotateCopy(rot90, ROTATION_90), rot180); + + final Rect rot270 = rotateCopy(original, ROTATION_270); + assertEquals("rot270", new Rect(4, 1, 2, 3), rot270); + assertEquals("rot90(rot180)=rot270", rotateCopy(rot180, ROTATION_90), rot270); + } + + private static Rect rotateCopy(Rect insets, int rotationDelta) { + final Rect copy = new Rect(insets); + InsetUtils.rotateInsets(copy, rotationDelta); + return copy; + } }