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:
@@ -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) {
|
||||
|
||||
@@ -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). -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user