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
This commit is contained in:
Adrian Roos
2018-08-20 13:43:38 +02:00
parent d82da978c1
commit 8c28c7c2d9
11 changed files with 156 additions and 19 deletions

View File

@@ -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<Path, DisplayCutout> 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<Path, DisplayCutout> 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<Rect> rects) {

View File

@@ -2981,6 +2981,10 @@
-->
<bool name="config_fillMainBuiltInDisplayCutout">false</bool>
<!-- If true, and there is a cutout on the main built in display, the cutout will be masked
by shrinking the display such that it does not overlap the cutout area. -->
<bool name="config_maskMainBuiltInDisplayCutout">false</bool>
<!-- Ultrasound support for Mic/speaker path -->
<!-- Whether the default microphone audio source supports near-ultrasound frequencies
(range of 18 - 21 kHz). -->

View File

@@ -61,6 +61,15 @@
<!-- Margin at the edge of the screen to ignore touch events for in the windowshade. -->
<dimen name="status_bar_edge_ignore">5dp</dimen>
<!-- Default radius of the software rounded corners. -->
<dimen name="rounded_corner_radius">0dp</dimen>
<!-- Radius of the software rounded corners at the top of the display in its natural
orientation. If zero, the value of rounded_corner_radius is used. -->
<dimen name="rounded_corner_radius_top">0dp</dimen>
<!-- Radius of the software rounded corners at the bottom of the display in its natural
orientation. If zero, the value of rounded_corner_radius is used. -->
<dimen name="rounded_corner_radius_bottom">0dp</dimen>
<!-- Width of the window of the divider bar used to resize docked stacks. -->
<dimen name="docked_stack_divider_thickness">48dp</dimen>

View File

@@ -3407,6 +3407,8 @@
<java-symbol type="integer" name="config_defaultHapticFeedbackIntensity" />
<java-symbol type="integer" name="config_defaultNotificationVibrationIntensity" />
<java-symbol type="bool" name="config_maskMainBuiltInDisplayCutout" />
<java-symbol type="array" name="config_disableApksUnlessMatchedSku_apk_list" />
<java-symbol type="array" name="config_disableApkUnlessMatchedSku_skus_list" />
</resources>

View File

@@ -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();

View File

@@ -939,9 +939,9 @@
<dimen name="bottom_padding">48dp</dimen>
<dimen name="edge_margin">8dp</dimen>
<dimen name="rounded_corner_radius">0dp</dimen>
<dimen name="rounded_corner_radius_top">0dp</dimen>
<dimen name="rounded_corner_radius_bottom">0dp</dimen>
<dimen name="rounded_corner_radius">@*android:dimen/rounded_corner_radius</dimen>
<dimen name="rounded_corner_radius_top">@*android:dimen/rounded_corner_radius_top</dimen>
<dimen name="rounded_corner_radius_bottom">@*android:dimen/rounded_corner_radius_bottom</dimen>
<dimen name="rounded_corner_content_padding">0dp</dimen>
<dimen name="nav_content_padding">0dp</dimen>
<dimen name="nav_quick_scrub_track_edge_padding">24dp</dimen>

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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}.
*/

View File

@@ -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;
}
}