diff options
| author | 2023-12-28 11:09:17 +0000 | |
|---|---|---|
| committer | 2023-12-28 11:09:17 +0000 | |
| commit | d2e0cd602bb985ee64689a8bf443b019d6455781 (patch) | |
| tree | 9399564eb170beac81ecfd6bba1cc11ee66a514d | |
| parent | 3fca7accd8335e7c4de81d6dab4dea9ff8c89176 (diff) | |
| parent | 8ce9cb3dc637b482af5c94b13176dc548ba5e430 (diff) | |
Merge "Support display cutout side override" into main
| -rw-r--r-- | core/java/android/view/DisplayCutout.java | 314 | ||||
| -rw-r--r-- | core/proto/android/view/displaycutout.proto | 1 | ||||
| -rw-r--r-- | core/res/res/values/config.xml | 19 | ||||
| -rw-r--r-- | core/res/res/values/symbols.xml | 3 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/view/DisplayCutoutTest.java | 132 |
5 files changed, 386 insertions, 83 deletions
diff --git a/core/java/android/view/DisplayCutout.java b/core/java/android/view/DisplayCutout.java index 712d1d67c942..f819c9b2f5dc 100644 --- a/core/java/android/view/DisplayCutout.java +++ b/core/java/android/view/DisplayCutout.java @@ -16,6 +16,7 @@ package android.view; +import static android.content.res.Resources.ID_NULL; import static android.util.DisplayMetrics.DENSITY_DEFAULT; import static android.util.DisplayMetrics.DENSITY_DEVICE_STABLE; import static android.view.DisplayCutoutProto.BOUND_BOTTOM; @@ -23,8 +24,12 @@ import static android.view.DisplayCutoutProto.BOUND_LEFT; import static android.view.DisplayCutoutProto.BOUND_RIGHT; import static android.view.DisplayCutoutProto.BOUND_TOP; import static android.view.DisplayCutoutProto.INSETS; +import static android.view.DisplayCutoutProto.SIDE_OVERRIDES; import static android.view.DisplayCutoutProto.WATERFALL_INSETS; import static android.view.Surface.ROTATION_0; +import static android.view.Surface.ROTATION_180; +import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; @@ -49,6 +54,7 @@ import android.view.Surface.Rotation; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.window.flags.Flags; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -112,6 +118,9 @@ public final class DisplayCutout { private static float sCachedPhysicalPixelDisplaySizeRatio; @GuardedBy("CACHE_LOCK") + private static int[] sCachedSideOverrides; + + @GuardedBy("CACHE_LOCK") private static CutoutPathParserInfo sCachedCutoutPathParserInfo; @GuardedBy("CACHE_LOCK") private static Path sCachedCutoutPath; @@ -150,6 +159,15 @@ public final class DisplayCutout { */ public static final int BOUNDS_POSITION_LENGTH = 4; + private static final int INVALID_SIDE_OVERRIDE = -1; + private static final String SIDE_STRING_TOP = "top"; + private static final String SIDE_STRING_BOTTOM = "bottom"; + private static final String SIDE_STRING_RIGHT = "right"; + private static final String SIDE_STRING_LEFT = "left"; + + // The side index is always under the natural rotation of the device. + private int[] mSideOverrides; + /** @hide */ @IntDef(prefix = { "BOUNDS_POSITION_" }, value = { BOUNDS_POSITION_LEFT, @@ -402,8 +420,8 @@ public final class DisplayCutout { // TODO(b/73953958): @VisibleForTesting(visibility = PRIVATE) public DisplayCutout(@NonNull Insets safeInsets, @Nullable Rect boundLeft, @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom) { - this(safeInsets.toRect(), Insets.NONE, boundLeft, boundTop, boundRight, boundBottom, null, - true); + this(getCopyOrRef(safeInsets.toRect(), true), Insets.NONE, + new Bounds(boundLeft, boundTop, boundRight, boundBottom, true), null, null); } /** @@ -426,11 +444,41 @@ public final class DisplayCutout { * @param info the cutout path parser info. * @hide */ + @VisibleForTesting public DisplayCutout(@NonNull Insets safeInsets, @Nullable Rect boundLeft, @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom, @NonNull Insets waterfallInsets, @Nullable CutoutPathParserInfo info) { - this(safeInsets.toRect(), waterfallInsets, boundLeft, boundTop, boundRight, boundBottom, - info, true); + this(getCopyOrRef(safeInsets.toRect(), true), waterfallInsets, + new Bounds(boundLeft, boundTop, boundRight, boundBottom, true), info, null); + } + + /** + * Creates a DisplayCutout instance. + * + * <p>Note that this is only useful for tests. For production code, developers should always + * use a {@link DisplayCutout} obtained from the system.</p> + * + * @param safeInsets the insets from each edge which avoid the display cutout as returned by + * {@link #getSafeInsetTop()} etc. + * @param boundLeft the left bounding rect of the display cutout in pixels. If null is passed, + * it's treated as an empty rectangle (0,0)-(0,0). + * @param boundTop the top bounding rect of the display cutout in pixels. If null is passed, + * it's treated as an empty rectangle (0,0)-(0,0). + * @param boundRight the right bounding rect of the display cutout in pixels. If null is + * passed, it's treated as an empty rectangle (0,0)-(0,0). + * @param boundBottom the bottom bounding rect of the display cutout in pixels. If null is + * passed, it's treated as an empty rectangle (0,0)-(0,0). + * @param waterfallInsets the insets for the curved areas in waterfall display. + * @param info the cutout path parser info. + * @hide + */ + public DisplayCutout(@NonNull Insets safeInsets, @Nullable Rect boundLeft, + @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom, + @NonNull Insets waterfallInsets, @Nullable CutoutPathParserInfo info, + @Nullable int[] sideOverrides) { + this(safeInsets.toRect(), waterfallInsets, + new Bounds(boundLeft, boundTop, boundRight, boundBottom, true), + info, sideOverrides); } /** @@ -454,8 +502,8 @@ public final class DisplayCutout { public DisplayCutout(@NonNull Insets safeInsets, @Nullable Rect boundLeft, @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom, @NonNull Insets waterfallInsets) { - this(safeInsets.toRect(), waterfallInsets, boundLeft, boundTop, boundRight, boundBottom, - null, true); + this(getCopyOrRef(safeInsets.toRect(), true), waterfallInsets, + new Bounds(boundLeft, boundTop, boundRight, boundBottom, true), null, null); } /** @@ -473,8 +521,8 @@ public final class DisplayCutout { // TODO(b/73953958): @VisibleForTesting(visibility = PRIVATE) @Deprecated public DisplayCutout(@Nullable Rect safeInsets, @Nullable List<Rect> boundingRects) { - this(safeInsets, Insets.NONE, extractBoundsFromList(safeInsets, boundingRects), null, - true /* copyArguments */); + this(getCopyOrRef(safeInsets, true), Insets.NONE, + new Bounds(extractBoundsFromList(safeInsets, boundingRects), true), null, null); } /** @@ -498,26 +546,29 @@ public final class DisplayCutout { private DisplayCutout(Rect safeInsets, Insets waterfallInsets, Rect boundLeft, Rect boundTop, Rect boundRight, Rect boundBottom, CutoutPathParserInfo info, boolean copyArguments) { - mSafeInsets = getCopyOrRef(safeInsets, copyArguments); - mWaterfallInsets = waterfallInsets == null ? Insets.NONE : waterfallInsets; - mBounds = new Bounds(boundLeft, boundTop, boundRight, boundBottom, copyArguments); - mCutoutPathParserInfo = info == null ? EMPTY_PARSER_INFO : info; + this(getCopyOrRef(safeInsets, copyArguments), waterfallInsets, + new Bounds(boundLeft, boundTop, boundRight, boundBottom, copyArguments), info, + null); } private DisplayCutout(Rect safeInsets, Insets waterfallInsets, Rect[] bounds, CutoutPathParserInfo info, boolean copyArguments) { - mSafeInsets = getCopyOrRef(safeInsets, copyArguments); - mWaterfallInsets = waterfallInsets == null ? Insets.NONE : waterfallInsets; - mBounds = new Bounds(bounds, copyArguments); - mCutoutPathParserInfo = info == null ? EMPTY_PARSER_INFO : info; + this(getCopyOrRef(safeInsets, copyArguments), waterfallInsets, + new Bounds(bounds, copyArguments), info, null); } private DisplayCutout(Rect safeInsets, Insets waterfallInsets, Bounds bounds, CutoutPathParserInfo info) { + this(safeInsets, waterfallInsets, bounds, info, null); + } + + private DisplayCutout(Rect safeInsets, Insets waterfallInsets, Bounds bounds, + CutoutPathParserInfo info, int[] sideOverrides) { mSafeInsets = safeInsets; mWaterfallInsets = waterfallInsets == null ? Insets.NONE : waterfallInsets; mBounds = bounds; mCutoutPathParserInfo = info == null ? EMPTY_PARSER_INFO : info; + mSideOverrides = sideOverrides; } private static Rect getCopyOrRef(Rect r, boolean copyArguments) { @@ -795,6 +846,7 @@ public final class DisplayCutout { result = 48271 * result + mBounds.hashCode(); result = 48271 * result + mWaterfallInsets.hashCode(); result = 48271 * result + mCutoutPathParserInfo.hashCode(); + result = 48271 * result + Arrays.hashCode(mSideOverrides); return result; } @@ -807,7 +859,8 @@ public final class DisplayCutout { DisplayCutout c = (DisplayCutout) o; return mSafeInsets.equals(c.mSafeInsets) && mBounds.equals(c.mBounds) && mWaterfallInsets.equals(c.mWaterfallInsets) - && mCutoutPathParserInfo.equals(c.mCutoutPathParserInfo); + && mCutoutPathParserInfo.equals(c.mCutoutPathParserInfo) + && Arrays.equals(mSideOverrides, c.mSideOverrides); } return false; } @@ -818,9 +871,48 @@ public final class DisplayCutout { + " waterfall=" + mWaterfallInsets + " boundingRect={" + mBounds + "}" + " cutoutPathParserInfo={" + mCutoutPathParserInfo + "}" + + " sideOverrides=" + sideOverridesToString(mSideOverrides) + "}"; } + private static String sideOverridesToString(int[] sideOverrides) { + if (sideOverrides == null) { + return "null"; + } + final StringBuilder sb = new StringBuilder(); + sb.append("{"); + final int length = sideOverrides.length; + if (length != BOUNDS_POSITION_LENGTH) { + sb.append("length=").append(sideOverrides.length).append(". "); + } + boolean hasContent = false; + for (int i = ROTATION_0; i < length; i++) { + final int override = sideOverrides[i]; + if (override != INVALID_SIDE_OVERRIDE) { + if (hasContent) { + sb.append(", "); + } + sb.append(Surface.rotationToString(i)).append(": "); + switch(override) { + case BOUNDS_POSITION_LEFT: + sb.append(SIDE_STRING_LEFT); + break; + case BOUNDS_POSITION_TOP: + sb.append(SIDE_STRING_TOP); + break; + case BOUNDS_POSITION_RIGHT: + sb.append(SIDE_STRING_RIGHT); + break; + case BOUNDS_POSITION_BOTTOM: + sb.append(SIDE_STRING_BOTTOM); + break; + } + hasContent = true; + } + } + return sb.append("}").toString(); + } + /** * @hide */ @@ -832,6 +924,11 @@ public final class DisplayCutout { mBounds.getRect(BOUNDS_POSITION_RIGHT).dumpDebug(proto, BOUND_RIGHT); mBounds.getRect(BOUNDS_POSITION_BOTTOM).dumpDebug(proto, BOUND_BOTTOM); mWaterfallInsets.toRect().dumpDebug(proto, WATERFALL_INSETS); + if (mSideOverrides != null) { + for (int sideOverride : mSideOverrides) { + proto.write(SIDE_OVERRIDES, sideOverride); + } + } proto.end(token); } @@ -899,7 +996,7 @@ public final class DisplayCutout { */ public DisplayCutout replaceSafeInsets(Rect safeInsets) { return new DisplayCutout(new Rect(safeInsets), mWaterfallInsets, mBounds, - mCutoutPathParserInfo); + mCutoutPathParserInfo, mSideOverrides); } private static int atLeastZero(int value) { @@ -1031,8 +1128,10 @@ public final class DisplayCutout { Insets insets; final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId); final TypedArray array = res.obtainTypedArray(R.array.config_waterfallCutoutArray); - if (index >= 0 && index < array.length() && array.getResourceId(index, 0) > 0) { - final int resourceId = array.getResourceId(index, 0); + final int resourceId = index >= 0 && index < array.length() + ? array.getResourceId(index, ID_NULL) + : ID_NULL; + if (resourceId != ID_NULL) { final TypedArray waterfall = res.obtainTypedArray(resourceId); insets = Insets.of( waterfall.getDimensionPixelSize(0 /* waterfall left edge size */, 0), @@ -1047,6 +1146,48 @@ public final class DisplayCutout { return insets; } + private static int[] getDisplayCutoutSideOverrides(Resources res, String displayUniqueId) + throws IllegalArgumentException { + if (!Flags.movableCutoutConfiguration()) { + return null; + } + final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId); + final TypedArray array = res.obtainTypedArray( + R.array.config_displayCutoutSideOverrideArray); + final int resourceId = index >= 0 && index < array.length() + ? array.getResourceId(index, ID_NULL) + : ID_NULL; + final String[] rawOverrides = resourceId != ID_NULL + ? array.getResources().getStringArray(resourceId) + : res.getStringArray(R.array.config_mainBuiltInDisplayCutoutSideOverride); + array.recycle(); + final int[] override = new int[]{INVALID_SIDE_OVERRIDE, INVALID_SIDE_OVERRIDE, + INVALID_SIDE_OVERRIDE, INVALID_SIDE_OVERRIDE}; + for (String rawOverride : rawOverrides) { + int rotation; + String[] split = rawOverride.split(" *, *"); + switch (split[0]) { + case "0" -> rotation = ROTATION_0; + case "90" -> rotation = ROTATION_90; + case "180" -> rotation = ROTATION_180; + case "270" -> rotation = ROTATION_270; + default -> throw new IllegalArgumentException("Invalid side override definition: " + + rawOverride); + } + int side; + switch (split[1]) { + case SIDE_STRING_LEFT -> side = BOUNDS_POSITION_LEFT; + case SIDE_STRING_TOP -> side = BOUNDS_POSITION_TOP; + case SIDE_STRING_RIGHT -> side = BOUNDS_POSITION_RIGHT; + case SIDE_STRING_BOTTOM -> side = BOUNDS_POSITION_BOTTOM; + default -> throw new IllegalArgumentException("Invalid side override definition: " + + rawOverride); + } + override[rotation] = side; + } + return override; + } + /** * Creates the display cutout according to * @android:string/config_mainBuiltInDisplayCutoutRectApproximation, which is the closest @@ -1060,7 +1201,8 @@ public final class DisplayCutout { getDisplayCutoutApproximationRect(res, displayUniqueId), physicalDisplayWidth, physicalDisplayHeight, displayWidth, displayHeight, DENSITY_DEVICE_STABLE / (float) DENSITY_DEFAULT, - getWaterfallInsets(res, displayUniqueId)).second; + getWaterfallInsets(res, displayUniqueId), + getDisplayCutoutSideOverrides(res, displayUniqueId)).second; } /** @@ -1070,17 +1212,17 @@ public final class DisplayCutout { */ @VisibleForTesting(visibility = PRIVATE) public static DisplayCutout fromSpec(String pathSpec, int displayWidth, - int displayHeight, float density, Insets waterfallInsets) { + int displayHeight, float density, Insets waterfallInsets, int[] sideOverrides) { return pathAndDisplayCutoutFromSpec( pathSpec, null, displayWidth, displayHeight, displayWidth, displayHeight, density, - waterfallInsets).second; + waterfallInsets, sideOverrides).second; } /** * Gets the cutout path and the corresponding DisplayCutout instance from the spec string. * - * @param pathSpec the spec string read from config_mainBuiltInDisplayCutout. - * @param rectSpec the spec string read from config_mainBuiltInDisplayCutoutRectApproximation. + * @param pathSpec the spec string read from config for certain display. + * @param rectSpec the rect approximation spec string read from config for certain display. * @param physicalDisplayWidth the max physical display width the display supports. * @param physicalDisplayHeight the max physical display height the display supports. * @param displayWidth the display width. @@ -1091,7 +1233,8 @@ public final class DisplayCutout { */ private static Pair<Path, DisplayCutout> pathAndDisplayCutoutFromSpec( String pathSpec, String rectSpec, int physicalDisplayWidth, int physicalDisplayHeight, - int displayWidth, int displayHeight, float density, Insets waterfallInsets) { + int displayWidth, int displayHeight, float density, Insets waterfallInsets, + int[] sideOverrides) { // Always use the rect approximation spec to create the cutout if it's not null because // transforming and sending a Region constructed from a path is very costly. String spec = rectSpec != null ? rectSpec : pathSpec; @@ -1107,7 +1250,8 @@ public final class DisplayCutout { && sCachedDisplayHeight == displayHeight && sCachedDensity == density && waterfallInsets.equals(sCachedWaterfallInsets) - && sCachedPhysicalPixelDisplaySizeRatio == physicalPixelDisplaySizeRatio) { + && sCachedPhysicalPixelDisplaySizeRatio == physicalPixelDisplaySizeRatio + && Arrays.equals(sCachedSideOverrides, sideOverrides)) { return sCachedCutout; } } @@ -1123,7 +1267,6 @@ public final class DisplayCutout { final Rect boundRight = cutoutSpec.getRightBound(); final Rect boundBottom = cutoutSpec.getBottomBound(); - if (!waterfallInsets.equals(Insets.NONE)) { safeInset.set( Math.max(waterfallInsets.left, safeInset.left), @@ -1135,10 +1278,21 @@ public final class DisplayCutout { final CutoutPathParserInfo cutoutPathParserInfo = new CutoutPathParserInfo( displayWidth, displayHeight, physicalDisplayWidth, physicalDisplayHeight, density, pathSpec.trim(), ROTATION_0, 1f /* scale */, physicalPixelDisplaySizeRatio); + final int sideOverride = getSideOverride(sideOverrides, ROTATION_0); + final Rect[] bounds = new Bounds(boundLeft, boundTop, boundRight, boundBottom, false) + .getRects(); + final int rotateDistance = getRotationToOverride(sideOverride, bounds, + ROTATION_0 /* defaultRotation */); + if (rotateDistance != ROTATION_0) { + Collections.rotate(Arrays.asList(bounds), rotateDistance); + } + final Rect safeInsets = DisplayCutout.computeSafeInsets(displayWidth, displayHeight, + waterfallInsets, bounds); + final DisplayCutout cutout = new DisplayCutout(safeInsets, waterfallInsets, + new Bounds(bounds[BOUNDS_POSITION_LEFT], bounds[BOUNDS_POSITION_TOP], + bounds[BOUNDS_POSITION_RIGHT], bounds[BOUNDS_POSITION_BOTTOM], false), + cutoutPathParserInfo, sideOverrides); - final DisplayCutout cutout = new DisplayCutout( - safeInset, waterfallInsets, boundLeft, boundTop, boundRight, boundBottom, - cutoutPathParserInfo , false /* copyArguments */); final Pair<Path, DisplayCutout> result = new Pair<>(cutoutSpec.getPath(), cutout); synchronized (CACHE_LOCK) { sCachedSpec = spec; @@ -1148,6 +1302,7 @@ public final class DisplayCutout { sCachedCutout = result; sCachedWaterfallInsets = waterfallInsets; sCachedPhysicalPixelDisplaySizeRatio = physicalPixelDisplaySizeRatio; + sCachedSideOverrides = sideOverrides; } return result; } @@ -1181,7 +1336,10 @@ public final class DisplayCutout { if (newBounds[i].isEmpty()) continue; RotationUtils.rotateBounds(newBounds[i], displayBounds, rotation); } - Collections.rotate(Arrays.asList(newBounds), -rotation); + final int defaultRotation = -rotation; + final int override = getSideOverride(mSideOverrides, toRotation); + Collections.rotate(Arrays.asList(newBounds), + getRotationToOverride(override, newBounds, defaultRotation)); final CutoutPathParserInfo info = getCutoutPathParserInfo(); final CutoutPathParserInfo newInfo = new CutoutPathParserInfo( info.getDisplayWidth(), info.getDisplayHeight(), info.getPhysicalDisplayWidth(), @@ -1193,51 +1351,87 @@ public final class DisplayCutout { final DisplayCutout tmp = DisplayCutout.constructDisplayCutout(newBounds, waterfallInsets, newInfo); final Rect safeInsets = DisplayCutout.computeSafeInsets(endWidth, endHeight, tmp); + tmp.mSideOverrides = mSideOverrides; return tmp.replaceSafeInsets(safeInsets); } + private static int getSideOverride(int[] sideOverrides, @Rotation int rotation) { + if (sideOverrides == null || sideOverrides.length != 4) { + return INVALID_SIDE_OVERRIDE; + } + return sideOverrides[rotation]; + } + + /** @return the rotation needed to rotate from the original side to the overridden one. */ + private static @Rotation int getRotationToOverride(int sideOverride, Rect[] bounds, + @Rotation int defaultRotation) { + if (sideOverride == INVALID_SIDE_OVERRIDE) { + return defaultRotation; + } + int side = -1; + for (int i = 0; i <= BOUNDS_POSITION_BOTTOM; i++) { + if (bounds[i].isEmpty()) { + continue; + } + if (side != -1) { + // We don't rotate at all when there are multiple non empty cutout bounds. + return defaultRotation; + } + side = i; + } + if (side == -1) { + return defaultRotation; + } + int rotation = sideOverride - side; + if (rotation < 0) { + rotation += 4; + } + return rotation; + } + /** * Compute the insets derived from a cutout. This is usually used to populate the safe-insets * of the cutout via {@link #replaceSafeInsets}. * @hide */ public static Rect computeSafeInsets(int displayW, int displayH, DisplayCutout cutout) { + return computeSafeInsets(displayW, displayH, cutout.getWaterfallInsets(), + cutout.getBoundingRectsAll()); + } + + private static Rect computeSafeInsets(int displayW, int displayH, Insets waterFallInsets, + Rect[] bounds) { if (displayW == displayH) { throw new UnsupportedOperationException("not implemented: display=" + displayW + "x" - + displayH + " cutout=" + cutout); + + displayH + " bounding rects=" + Arrays.toString(bounds)); } - int leftInset = Math.max(cutout.getWaterfallInsets().left, findCutoutInsetForSide( - displayW, displayH, cutout.getBoundingRectLeft(), Gravity.LEFT)); - int topInset = Math.max(cutout.getWaterfallInsets().top, findCutoutInsetForSide( - displayW, displayH, cutout.getBoundingRectTop(), Gravity.TOP)); - int rightInset = Math.max(cutout.getWaterfallInsets().right, findCutoutInsetForSide( - displayW, displayH, cutout.getBoundingRectRight(), Gravity.RIGHT)); - int bottomInset = Math.max(cutout.getWaterfallInsets().bottom, findCutoutInsetForSide( - displayW, displayH, cutout.getBoundingRectBottom(), Gravity.BOTTOM)); + int leftInset = Math.max(waterFallInsets.left, findCutoutInsetForSide( + displayW, displayH, bounds[BOUNDS_POSITION_LEFT], Gravity.LEFT)); + int topInset = Math.max(waterFallInsets.top, findCutoutInsetForSide( + displayW, displayH, bounds[BOUNDS_POSITION_TOP], Gravity.TOP)); + int rightInset = Math.max(waterFallInsets.right, findCutoutInsetForSide( + displayW, displayH, bounds[BOUNDS_POSITION_RIGHT], Gravity.RIGHT)); + int bottomInset = Math.max(waterFallInsets.bottom, findCutoutInsetForSide( + displayW, displayH, bounds[BOUNDS_POSITION_BOTTOM], Gravity.BOTTOM)); return new Rect(leftInset, topInset, rightInset, bottomInset); } - private static int findCutoutInsetForSide(int displayW, int displayH, Rect boundingRect, - int gravity) { + private static int findCutoutInsetForSide(int displayW, int displayH, + @NonNull Rect boundingRect, int gravity) { if (boundingRect.isEmpty()) { return 0; } int inset = 0; - switch (gravity) { - case Gravity.TOP: - return Math.max(inset, boundingRect.bottom); - case Gravity.BOTTOM: - return Math.max(inset, displayH - boundingRect.top); - case Gravity.LEFT: - return Math.max(inset, boundingRect.right); - case Gravity.RIGHT: - return Math.max(inset, displayW - boundingRect.left); - default: - throw new IllegalArgumentException("unknown gravity: " + gravity); - } + return switch (gravity) { + case Gravity.TOP -> Math.max(inset, boundingRect.bottom); + case Gravity.BOTTOM -> Math.max(inset, displayH - boundingRect.top); + case Gravity.LEFT -> Math.max(inset, boundingRect.right); + case Gravity.RIGHT -> Math.max(inset, displayW - boundingRect.left); + default -> throw new IllegalArgumentException("unknown gravity: " + gravity); + }; } /** @@ -1293,6 +1487,7 @@ public final class DisplayCutout { out.writeInt(cutout.mCutoutPathParserInfo.getRotation()); out.writeFloat(cutout.mCutoutPathParserInfo.getScale()); out.writeFloat(cutout.mCutoutPathParserInfo.getPhysicalPixelDisplaySizeRatio()); + out.writeIntArray(cutout.mSideOverrides); } } @@ -1348,9 +1543,10 @@ public final class DisplayCutout { final CutoutPathParserInfo info = new CutoutPathParserInfo( displayWidth, displayHeight, physicalDisplayWidth, physicalDisplayHeight, density, cutoutSpec, rotation, scale, physicalPixelDisplaySizeRatio); + final int[] sideOverrides = in.createIntArray(); - return new DisplayCutout( - safeInsets, waterfallInsets, bounds, info, false /* copyArguments */); + return new DisplayCutout(safeInsets, waterfallInsets, + new Bounds(bounds, false /* copyArguments */), info, sideOverrides); } public DisplayCutout get() { @@ -1382,8 +1578,10 @@ public final class DisplayCutout { mInner.mCutoutPathParserInfo.getRotation(), scale, mInner.mCutoutPathParserInfo.getPhysicalPixelDisplaySizeRatio()); + final int[] sideOverrides = mInner.mSideOverrides; - mInner = new DisplayCutout(safeInsets, Insets.of(waterfallInsets), bounds, info); + mInner = new DisplayCutout(safeInsets, Insets.of(waterfallInsets), bounds, info, + sideOverrides); } @Override diff --git a/core/proto/android/view/displaycutout.proto b/core/proto/android/view/displaycutout.proto index 72d5303a6f2c..53155d2d46c6 100644 --- a/core/proto/android/view/displaycutout.proto +++ b/core/proto/android/view/displaycutout.proto @@ -32,4 +32,5 @@ message DisplayCutoutProto { optional .android.graphics.RectProto bound_right = 5; optional .android.graphics.RectProto bound_bottom = 6; optional .android.graphics.RectProto waterfall_insets = 7; + repeated int32 side_overrides = 8; } diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 804e9ef8476b..806be9471ae5 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -4017,6 +4017,18 @@ by shrinking the display such that it does not overlap the cutout area. --> <bool name="config_maskMainBuiltInDisplayCutout">false</bool> + <!-- This string array provide override side of each rotation of the given insets. + Array of "[rotation],[side]". + Undefined rotation will apply the default behavior. + When there are cutouts on multiple edges of the display, the override won't take any + effect. --> + <string-array name="config_mainBuiltInDisplayCutoutSideOverride" translatable="false"> + <!-- Example: + <item>90,top</item> + <item>270,bottom</item> + --> + </string-array> + <!-- Ultrasound support for Mic/speaker path --> <!-- Whether the default microphone audio source supports near-ultrasound frequencies (range of 18 - 21 kHz). --> @@ -6373,6 +6385,8 @@ </string> <bool name="config_fillSecondaryBuiltInDisplayCutout">false</bool> <bool name="config_maskSecondaryBuiltInDisplayCutout">false</bool> + <string-array name="config_secondaryBuiltInDisplayCutoutSideOverride" translatable="false"> + </string-array> <!-- An array contains unique ids of all built-in displays and the unique id of a display can be obtained from {@link Display#getUniqueId}. This array should be set for multi-display @@ -6418,6 +6432,11 @@ <item>@string/config_secondaryBuiltInDisplayCutoutRectApproximation</item> </string-array> + <array name="config_displayCutoutSideOverrideArray" translatable="false"> + <item>@array/config_mainBuiltInDisplayCutoutSideOverride</item> + <item>@array/config_secondaryBuiltInDisplayCutoutSideOverride</item> + </array> + <!-- The maskBuiltInDisplayCutout config for each display in a multi-display device. --> <array name="config_maskBuiltInDisplayCutoutArray" translatable="false"> <item>@bool/config_maskMainBuiltInDisplayCutout</item> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 9589fb00fd5d..b0a4c16b0551 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4036,6 +4036,7 @@ <java-symbol type="string" name="global_action_logout" /> <java-symbol type="string" name="config_mainBuiltInDisplayCutout" /> <java-symbol type="string" name="config_mainBuiltInDisplayCutoutRectApproximation" /> + <java-symbol type="array" name="config_mainBuiltInDisplayCutoutSideOverride" /> <java-symbol type="drawable" name="messaging_user" /> <java-symbol type="bool" name="config_fillMainBuiltInDisplayCutout" /> <java-symbol type="drawable" name="ic_logout" /> @@ -5002,9 +5003,11 @@ <java-symbol type="string" name="config_secondaryBuiltInDisplayCutoutRectApproximation" /> <java-symbol type="bool" name="config_fillSecondaryBuiltInDisplayCutout" /> <java-symbol type="bool" name="config_maskSecondaryBuiltInDisplayCutout" /> + <java-symbol type="array" name="config_secondaryBuiltInDisplayCutoutSideOverride" /> <java-symbol type="array" name="config_displayUniqueIdArray" /> <java-symbol type="array" name="config_displayCutoutPathArray" /> <java-symbol type="array" name="config_displayCutoutApproximationRectArray" /> + <java-symbol type="array" name="config_displayCutoutSideOverrideArray" /> <java-symbol type="array" name="config_fillBuiltInDisplayCutoutArray" /> <java-symbol type="array" name="config_maskBuiltInDisplayCutoutArray" /> <java-symbol type="dimen" name="secondary_waterfall_display_left_edge_size" /> diff --git a/core/tests/coretests/src/android/view/DisplayCutoutTest.java b/core/tests/coretests/src/android/view/DisplayCutoutTest.java index faeae2cb6698..0d1dde38ca8f 100644 --- a/core/tests/coretests/src/android/view/DisplayCutoutTest.java +++ b/core/tests/coretests/src/android/view/DisplayCutoutTest.java @@ -16,6 +16,8 @@ package android.view; +import static android.view.DisplayCutout.BOUNDS_POSITION_BOTTOM; +import static android.view.DisplayCutout.BOUNDS_POSITION_TOP; import static android.view.DisplayCutout.NO_CUTOUT; import static android.view.DisplayCutout.extractBoundsFromList; import static android.view.DisplayCutout.fromSpec; @@ -180,7 +182,7 @@ public class DisplayCutoutTest { final int displayHeight = 400; final float density = 1f; final DisplayCutout cutout = fromSpec(cutoutSpecString, displayWidth, displayHeight, - density, Insets.NONE); + density, Insets.NONE, null); assertThat(cutout.getCutoutPath(), notNullValue()); } @@ -191,9 +193,9 @@ public class DisplayCutoutTest { final int displayHeight = 400; final float density = 1f; final Path first = fromSpec(cutoutSpecString, displayWidth, displayHeight, - density, Insets.NONE).getCutoutPath(); + density, Insets.NONE, null).getCutoutPath(); final Path second = fromSpec(cutoutSpecString, displayWidth, displayHeight, - density, Insets.NONE).getCutoutPath(); + density, Insets.NONE, null).getCutoutPath(); assertThat(first, equalTo(second)); } @@ -203,9 +205,9 @@ public class DisplayCutoutTest { final int displayHeight = 400; final float density = 1f; final Path first = fromSpec("L1,0 L1,1 L0,1 z", displayWidth, displayHeight, - density, Insets.NONE).getCutoutPath(); + density, Insets.NONE, null).getCutoutPath(); final Path second = fromSpec("L2,0 L2,2 L0,2 z", displayWidth, displayHeight, - density, Insets.NONE).getCutoutPath(); + density, Insets.NONE, null).getCutoutPath(); assertThat(first, not(equalTo(second))); } @@ -216,7 +218,7 @@ public class DisplayCutoutTest { final int displayHeight = 400; final float density = 1f; final DisplayCutout cutout = fromSpec(cutoutSpecString, displayWidth, displayHeight, - density, Insets.NONE); + density, Insets.NONE, null); assertThat(displayWidth, equalTo(cutout.getCutoutPathParserInfo().getDisplayWidth())); assertThat(displayHeight, equalTo(cutout.getCutoutPathParserInfo().getDisplayHeight())); assertThat(density, equalTo(cutout.getCutoutPathParserInfo().getDensity())); @@ -360,63 +362,64 @@ public class DisplayCutoutTest { @Test public void fromSpec_caches() { Insets waterfallInsets = Insets.of(0, 20, 0, 20); - DisplayCutout cached = fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 1f, waterfallInsets); + DisplayCutout cached = fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 1f, waterfallInsets, null); assertThat( - fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 1f, waterfallInsets), + fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 1f, waterfallInsets, null), sameInstance(cached)); } @Test public void fromSpec_wontCacheIfSpecChanges() { - DisplayCutout cached = fromSpec("L1,0 L1000,1000 L0,1 z", 200, 400, 1f, Insets.NONE); + DisplayCutout cached = fromSpec("L1,0 L1000,1000 L0,1 z", 200, 400, 1f, Insets.NONE, null); assertThat( - fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 1f, Insets.NONE), + fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 1f, Insets.NONE, null), not(sameInstance(cached))); } @Test public void fromSpec_wontCacheIfScreenWidthChanges() { - DisplayCutout cached = fromSpec("L1,0 L1,1 L0,1 z", 2000, 400, 1f, Insets.NONE); + DisplayCutout cached = fromSpec("L1,0 L1,1 L0,1 z", 2000, 400, 1f, Insets.NONE, null); assertThat( - fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 1f, Insets.NONE), + fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 1f, Insets.NONE, null), not(sameInstance(cached))); } @Test public void fromSpec_wontCacheIfScreenHeightChanges() { - DisplayCutout cached = fromSpec("L1,0 L1,1 L0,1 z", 200, 4000, 1f, Insets.NONE); + DisplayCutout cached = fromSpec("L1,0 L1,1 L0,1 z", 200, 4000, 1f, Insets.NONE, null); assertThat( - fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 1f, Insets.NONE), + fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 1f, Insets.NONE, null), not(sameInstance(cached))); } @Test public void fromSpec_wontCacheIfDensityChanges() { - DisplayCutout cached = fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 2f, Insets.NONE); + DisplayCutout cached = fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 2f, Insets.NONE, null); assertThat( - fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 1f, Insets.NONE), + fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 1f, Insets.NONE, null), not(sameInstance(cached))); } @Test public void fromSpec_wontCacheIfWaterfallInsetsChange() { Insets waterfallInsets = Insets.of(0, 20, 0, 20); - DisplayCutout cached = fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 2f, Insets.NONE); + DisplayCutout cached = fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 2f, Insets.NONE, null); assertThat( - fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 2f, waterfallInsets), + fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 2f, waterfallInsets, null), 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, Insets.NONE); + DisplayCutout cutout = fromSpec("M -50,0 v 20 h 100 v -20 z", 200, 400, 2f, + Insets.NONE, null); 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, Insets.NONE); + + "@bottom M -50,0 v -10,0 h 100 v 20 z", 200, 400, 2f, Insets.NONE, null); assertThat(cutout.getSafeInsets(), equalTo(new Rect(0, 20, 0, 10))); assertThat(cutout.getBoundingRectsAll(), equalTo(new Rect[]{ ZERO_RECT, new Rect(50, 0, 150, 20), @@ -426,33 +429,35 @@ public class DisplayCutoutTest { @Test public void fromSpec_setsSafeInsets_waterfallTopBottom() { - DisplayCutout cutout = fromSpec("", 200, 400, 2f, Insets.of(0, 30, 0, 30)); + DisplayCutout cutout = fromSpec("", 200, 400, 2f, Insets.of(0, 30, 0, 30), null); assertThat(cutout.getSafeInsets(), equalTo(new Rect(0, 30, 0, 30))); } @Test public void fromSpec_setsSafeInsets_waterfallLeftRight() { - DisplayCutout cutout = fromSpec("", 200, 400, 2f, Insets.of(30, 0, 30, 0)); + DisplayCutout cutout = fromSpec("", 200, 400, 2f, Insets.of(30, 0, 30, 0), null); assertThat(cutout.getSafeInsets(), equalTo(new Rect(30, 0, 30, 0))); } @Test public void fromSpec_setsSafeInsets_waterfall_allEdges() { - DisplayCutout cutout = fromSpec("", 200, 400, 2f, Insets.of(30, 30, 30, 30)); + DisplayCutout cutout = fromSpec("", 200, 400, 2f, Insets.of(30, 30, 30, 30), null); assertThat(cutout.getSafeInsets(), equalTo(new Rect(30, 30, 30, 30))); } @Test public void fromSpec_setsSafeInsets_cutoutTopBottom_waterfallTopBottom() { DisplayCutout cutout = fromSpec("M -50,0 v 20 h 100 v -20 z" - + "@bottom M -50,0 v -20,0 h 100 v 20 z", 200, 400, 2f, Insets.of(0, 30, 0, 30)); + + "@bottom M -50,0 v -20,0 h 100 v 20 z", 200, 400, 2f, + Insets.of(0, 30, 0, 30), null); assertThat(cutout.getSafeInsets(), equalTo(new Rect(0, 30, 0, 30))); } @Test public void fromSpec_setsSafeInsets_cutoutTopBottom_waterfallLeftRight() { DisplayCutout cutout = fromSpec("M -50,0 v 20 h 100 v -20 z" - + "@bottom M -50,0 v -20,0 h 100 v 20 z", 200, 400, 2f, Insets.of(30, 0, 30, 0)); + + "@bottom M -50,0 v -20,0 h 100 v 20 z", 200, 400, 2f, + Insets.of(30, 0, 30, 0), null); assertThat(cutout.getSafeInsets(), equalTo(new Rect(30, 20, 30, 20))); } @@ -568,7 +573,84 @@ public class DisplayCutoutTest { DisplayCutout rotated = cutout.getRotated(displayH, displayW, ROTATION_90, ROTATION_180); assertEquals(expected, rotated); } + @Test + public void testGetRotatedCutoutWithOverride_top_rot0() { + int displayW = 500, displayH = 1000; + final int[] sideOverrides = new int[] {BOUNDS_POSITION_TOP, BOUNDS_POSITION_BOTTOM, + BOUNDS_POSITION_BOTTOM, BOUNDS_POSITION_TOP}; + DisplayCutout expected = new DisplayCutout(Insets.of(20, 100, 20, 0), + ZERO_RECT, new Rect(50, 0, 75, 100), ZERO_RECT, ZERO_RECT, + Insets.of(20, 0, 20, 0), null, sideOverrides); + DisplayCutout cutout = new DisplayCutout(Insets.of(20, 100, 20, 0), + ZERO_RECT, new Rect(50, 0, 75, 100), ZERO_RECT, ZERO_RECT, + Insets.of(20, 0, 20, 0), null, sideOverrides); + DisplayCutout rotated = cutout.getRotated(displayW, displayH, ROTATION_0, ROTATION_0); + assertEquals(expected, rotated); + } + @Test + public void testGetRotatedCutoutWithOverride_top_rot90() { + int displayW = 500, displayH = 1000; + final int[] sideOverrides = new int[] {BOUNDS_POSITION_TOP, BOUNDS_POSITION_BOTTOM, + BOUNDS_POSITION_BOTTOM, BOUNDS_POSITION_TOP}; + DisplayCutout expected = new DisplayCutout(Insets.of(0, 20, 0, 75), + ZERO_RECT, ZERO_RECT, ZERO_RECT, new Rect(0, displayW - 75, 100, displayW - 50), + Insets.of(0, 20, 0, 20), createParserInfo(ROTATION_90), sideOverrides); + DisplayCutout cutout = new DisplayCutout(Insets.of(20, 100, 20, 0), + ZERO_RECT, new Rect(50, 0, 75, 100), ZERO_RECT, ZERO_RECT, + Insets.of(20, 0, 20, 0), null, sideOverrides); + DisplayCutout rotated = cutout.getRotated(displayW, displayH, ROTATION_0, ROTATION_90); + assertEquals(expected, rotated); + } + + @Test + public void testGetRotatedCutoutWithOverride_top_rot180() { + int displayW = 500, displayH = 1000; + final int[] sideOverrides = new int[] {BOUNDS_POSITION_TOP, BOUNDS_POSITION_BOTTOM, + BOUNDS_POSITION_BOTTOM, BOUNDS_POSITION_TOP}; + DisplayCutout expected = new DisplayCutout(Insets.of(20, 0, 20, 100), + ZERO_RECT, ZERO_RECT, ZERO_RECT, + new Rect(displayW - 75, displayH - 100, displayW - 50, displayH - 0), + Insets.of(20, 0, 20, 0), createParserInfo(ROTATION_180), sideOverrides); + DisplayCutout cutout = new DisplayCutout(Insets.of(20, 100, 20, 0), + ZERO_RECT, new Rect(50, 0, 75, 100), ZERO_RECT, ZERO_RECT, + Insets.of(20, 0, 20, 0), null, sideOverrides); + DisplayCutout rotated = cutout.getRotated(displayW, displayH, ROTATION_0, ROTATION_180); + assertEquals(expected, rotated); + } + + @Test + public void testGetRotatedCutoutWithOverride_top_rot270() { + int displayW = 500, displayH = 1000; + final int[] sideOverrides = new int[] {BOUNDS_POSITION_TOP, BOUNDS_POSITION_BOTTOM, + BOUNDS_POSITION_BOTTOM, BOUNDS_POSITION_TOP}; + DisplayCutout expected = new DisplayCutout(Insets.of(0, 75, 0, 20), + ZERO_RECT, new Rect(displayH - 100, 50, displayH - 0, 75), ZERO_RECT, ZERO_RECT, + Insets.of(0, 20, 0, 20), createParserInfo(ROTATION_270), sideOverrides); + DisplayCutout cutout = new DisplayCutout(Insets.of(20, 100, 20, 0), + ZERO_RECT, new Rect(50, 0, 75, 100), ZERO_RECT, ZERO_RECT, + Insets.of(20, 0, 20, 0), null, sideOverrides); + DisplayCutout rotated = cutout.getRotated(displayW, displayH, ROTATION_0, ROTATION_270); + assertEquals(expected, rotated); + } + + @Test + public void testGetRotatedCutoutWithOverride_top_rot90to180() { + int displayW = 500, displayH = 1000; + final int[] sideOverrides = new int[] {BOUNDS_POSITION_TOP, BOUNDS_POSITION_BOTTOM, + BOUNDS_POSITION_BOTTOM, BOUNDS_POSITION_TOP}; + DisplayCutout expected = new DisplayCutout(Insets.of(20, 0, 20, 100), + ZERO_RECT, ZERO_RECT, ZERO_RECT, + new Rect(displayW - 75, displayH - 100, displayW - 50, displayH - 0), + Insets.of(20, 0, 20, 0), createParserInfo(ROTATION_180), + sideOverrides); + DisplayCutout cutout = new DisplayCutout(Insets.of(0, 20, 0, 75), + ZERO_RECT, ZERO_RECT, ZERO_RECT, new Rect(0, displayW - 75, 100, displayW - 50), + Insets.of(0, 20, 0, 20), null, sideOverrides); + // starting from 90, so the start displayW/H are swapped: + DisplayCutout rotated = cutout.getRotated(displayH, displayW, ROTATION_90, ROTATION_180); + assertEquals(expected, rotated); + } private static DisplayCutout createCutoutTop() { return createCutoutWithInsets(0, 100, 0, 0); } |