From 9382674db2e9dc13b44f51ad1419ce52a577377f Mon Sep 17 00:00:00 2001 From: Leon Scroggins III Date: Tue, 25 Jan 2022 12:57:15 -0500 Subject: [PATCH] ScreenDecorHwcLayer: request and report transparent regions I6c5d29614dc8d65d8481150ce717a440192083a0 plumbs the transparent region to the HAL blocking region for DISPLAY_DECORATION layers. Make ScreenDecorHwcLayer call requestTransparentRegion, which will make the ViewRootImpl compute the transparent region. Then override gatherTransparentRegion to remove sections where it draws. Bug: 212736475 Test: atest ScreenDecorHwcLayerTest Change-Id: I0debc8a96dea34ff03ec089233523fe279ecbb00 --- .../android/systemui/DisplayCutoutBaseView.kt | 7 +- .../android/systemui/ScreenDecorHwcLayer.kt | 184 +++++++++++++++++- .../android/systemui/ScreenDecorations.java | 18 +- .../systemui/ScreenDecorHwcLayerTest.kt | 182 +++++++++++++++++ 4 files changed, 381 insertions(+), 10 deletions(-) create mode 100644 packages/SystemUI/tests/src/com/android/systemui/ScreenDecorHwcLayerTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt b/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt index 6bec8aacd55fe..56046d9c057d8 100644 --- a/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt +++ b/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt @@ -49,7 +49,7 @@ open class DisplayCutoutBaseView : View, RegionInterceptableView { private val shouldDrawCutout: Boolean = DisplayCutout.getFillBuiltInDisplayCutout( context.resources, context.display?.uniqueId) private var displayMode: Display.Mode? = null - private val location = IntArray(2) + protected val location = IntArray(2) protected var displayRotation = 0 @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) @@ -65,7 +65,8 @@ open class DisplayCutoutBaseView : View, RegionInterceptableView { @JvmField val protectionPath: Path = Path() private val protectionRectOrig: RectF = RectF() private val protectionPathOrig: Path = Path() - private var cameraProtectionProgress: Float = HIDDEN_CAMERA_PROTECTION_SCALE + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + var cameraProtectionProgress: Float = HIDDEN_CAMERA_PROTECTION_SCALE private var cameraProtectionAnimator: ValueAnimator? = null constructor(context: Context) : super(context) @@ -273,4 +274,4 @@ open class DisplayCutoutBaseView : View, RegionInterceptableView { } } } -} \ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorHwcLayer.kt b/packages/SystemUI/src/com/android/systemui/ScreenDecorHwcLayer.kt index ee1d9a3aa5de4..4b868622ce58d 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorHwcLayer.kt +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorHwcLayer.kt @@ -26,11 +26,22 @@ import android.graphics.PixelFormat import android.graphics.PorterDuff import android.graphics.PorterDuffColorFilter import android.graphics.PorterDuffXfermode +import android.graphics.Rect +import android.graphics.Region import android.graphics.drawable.Drawable import android.hardware.graphics.common.AlphaInterpretation import android.hardware.graphics.common.DisplayDecorationSupport +import android.view.DisplayCutout.BOUNDS_POSITION_BOTTOM +import android.view.DisplayCutout.BOUNDS_POSITION_LEFT +import android.view.DisplayCutout.BOUNDS_POSITION_LENGTH +import android.view.DisplayCutout.BOUNDS_POSITION_TOP +import android.view.DisplayCutout.BOUNDS_POSITION_RIGHT import android.view.RoundedCorner import android.view.RoundedCorners +import android.view.Surface +import androidx.annotation.VisibleForTesting +import kotlin.math.ceil +import kotlin.math.floor /** * When the HWC of the device supports Composition.DISPLAY_DECORATON, we use this layer to draw @@ -38,13 +49,16 @@ import android.view.RoundedCorners */ class ScreenDecorHwcLayer(context: Context, displayDecorationSupport: DisplayDecorationSupport) : DisplayCutoutBaseView(context) { - public val colorMode: Int + val colorMode: Int private val useInvertedAlphaColor: Boolean private val color: Int private val bgColor: Int private val cornerFilter: ColorFilter private val cornerBgFilter: ColorFilter private val clearPaint: Paint + @JvmField val transparentRect: Rect = Rect() + private val debugTransparentRegionPaint: Paint? + private val tempRect: Rect = Rect() private var roundedCornerTopSize = 0 private var roundedCornerBottomSize = 0 @@ -61,6 +75,10 @@ class ScreenDecorHwcLayer(context: Context, displayDecorationSupport: DisplayDec bgColor = Color.TRANSPARENT colorMode = ActivityInfo.COLOR_MODE_DEFAULT useInvertedAlphaColor = false + debugTransparentRegionPaint = Paint().apply { + color = 0x2f00ff00 // semi-transparent green + style = Paint.Style.FILL + } } else { colorMode = ActivityInfo.COLOR_MODE_A8 useInvertedAlphaColor = displayDecorationSupport.alphaInterpretation == @@ -72,6 +90,7 @@ class ScreenDecorHwcLayer(context: Context, displayDecorationSupport: DisplayDec color = Color.BLACK bgColor = Color.TRANSPARENT } + debugTransparentRegionPaint = null } cornerFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) cornerBgFilter = PorterDuffColorFilter(bgColor, PorterDuff.Mode.SRC_OUT) @@ -82,6 +101,9 @@ class ScreenDecorHwcLayer(context: Context, displayDecorationSupport: DisplayDec override fun onAttachedToWindow() { super.onAttachedToWindow() + if (!DEBUG_COLOR) { + parent.requestTransparentRegion(this) + } viewRootImpl.setDisplayDecoration(true) if (useInvertedAlphaColor) { @@ -93,12 +115,172 @@ class ScreenDecorHwcLayer(context: Context, displayDecorationSupport: DisplayDec } override fun onDraw(canvas: Canvas) { + // If updating onDraw, also update gatherTransparentRegion if (useInvertedAlphaColor) { canvas.drawColor(bgColor) } // Cutouts are drawn in DisplayCutoutBaseView.onDraw() super.onDraw(canvas) drawRoundedCorners(canvas) + + debugTransparentRegionPaint?.let { + calculateTransparentRect() + canvas.drawRect(transparentRect, it) + } + } + + override fun gatherTransparentRegion(region: Region?): Boolean { + region?.let { + calculateTransparentRect() + region.op(transparentRect, Region.Op.INTERSECT) + } + // Always return false - views underneath this should always be visible. + return false + } + + /** + * The transparent rect is calculated by subtracting the regions of cutouts, cutout protect and + * rounded corners from the region with fullscreen display size. + */ + @VisibleForTesting + fun calculateTransparentRect() { + transparentRect.set(0, 0, width, height) + + // Remove cutout region. + removeCutoutFromTransparentRegion() + + // Remove cutout protection region. + removeCutoutProtectionFromTransparentRegion() + + // Remove rounded corner region. + removeRoundedCornersFromTransparentRegion() + } + + private fun removeCutoutFromTransparentRegion() { + displayInfo.displayCutout?.let { + cutout -> + if (!cutout.boundingRectLeft.isEmpty) { + transparentRect.left = + cutout.boundingRectLeft.right.coerceAtLeast(transparentRect.left) + } + if (!cutout.boundingRectTop.isEmpty) { + transparentRect.top = + cutout.boundingRectTop.bottom.coerceAtLeast(transparentRect.top) + } + if (!cutout.boundingRectRight.isEmpty) { + transparentRect.right = + cutout.boundingRectRight.left.coerceAtMost(transparentRect.right) + } + if (!cutout.boundingRectBottom.isEmpty) { + transparentRect.bottom = + cutout.boundingRectBottom.top.coerceAtMost(transparentRect.bottom) + } + } + } + + private fun removeCutoutProtectionFromTransparentRegion() { + if (protectionRect.isEmpty) { + return + } + + val centerX = protectionRect.centerX() + val centerY = protectionRect.centerY() + val scaledDistanceX = (centerX - protectionRect.left) * cameraProtectionProgress + val scaledDistanceY = (centerY - protectionRect.top) * cameraProtectionProgress + tempRect.set( + floor(centerX - scaledDistanceX).toInt(), + floor(centerY - scaledDistanceY).toInt(), + ceil(centerX + scaledDistanceX).toInt(), + ceil(centerY + scaledDistanceY).toInt() + ) + + // Find out which edge the protectionRect belongs and remove that edge from the transparent + // region. + val leftDistance = tempRect.left + val topDistance = tempRect.top + val rightDistance = width - tempRect.right + val bottomDistance = height - tempRect.bottom + val minDistance = minOf(leftDistance, topDistance, rightDistance, bottomDistance) + when (minDistance) { + leftDistance -> { + transparentRect.left = tempRect.right.coerceAtLeast(transparentRect.left) + } + topDistance -> { + transparentRect.top = tempRect.bottom.coerceAtLeast(transparentRect.top) + } + rightDistance -> { + transparentRect.right = tempRect.left.coerceAtMost(transparentRect.right) + } + bottomDistance -> { + transparentRect.bottom = tempRect.top.coerceAtMost(transparentRect.bottom) + } + } + } + + private fun removeRoundedCornersFromTransparentRegion() { + var hasTopOrBottomCutouts = false + var hasLeftOrRightCutouts = false + displayInfo.displayCutout?.let { + cutout -> + hasTopOrBottomCutouts = !cutout.boundingRectTop.isEmpty || + !cutout.boundingRectBottom.isEmpty + hasLeftOrRightCutouts = !cutout.boundingRectLeft.isEmpty || + !cutout.boundingRectRight.isEmpty + } + // The goal is to remove the rounded corner areas as small as possible so that we can have a + // larger transparent region. Therefore, we should always remove from the short edge sides + // if possible. + val isShortEdgeTopBottom = width < height + if (isShortEdgeTopBottom) { + // Short edges on top & bottom. + if (!hasTopOrBottomCutouts && hasLeftOrRightCutouts) { + // If there are cutouts only on left or right edges, remove left and right sides + // for rounded corners. + transparentRect.left = getRoundedCornerSizeByPosition(BOUNDS_POSITION_LEFT) + .coerceAtLeast(transparentRect.left) + transparentRect.right = + (width - getRoundedCornerSizeByPosition(BOUNDS_POSITION_RIGHT)) + .coerceAtMost(transparentRect.right) + } else { + // If there are cutouts on top or bottom edges or no cutout at all, remove top + // and bottom sides for rounded corners. + transparentRect.top = getRoundedCornerSizeByPosition(BOUNDS_POSITION_TOP) + .coerceAtLeast(transparentRect.top) + transparentRect.bottom = + (height - getRoundedCornerSizeByPosition(BOUNDS_POSITION_BOTTOM)) + .coerceAtMost(transparentRect.bottom) + } + } else { + // Short edges on left & right. + if (hasTopOrBottomCutouts && !hasLeftOrRightCutouts) { + // If there are cutouts only on top or bottom edges, remove top and bottom sides + // for rounded corners. + transparentRect.top = getRoundedCornerSizeByPosition(BOUNDS_POSITION_TOP) + .coerceAtLeast(transparentRect.top) + transparentRect.bottom = + (height - getRoundedCornerSizeByPosition(BOUNDS_POSITION_BOTTOM)) + .coerceAtMost(transparentRect.bottom) + } else { + // If there are cutouts on left or right edges or no cutout at all, remove left + // and right sides for rounded corners. + transparentRect.left = getRoundedCornerSizeByPosition(BOUNDS_POSITION_LEFT) + .coerceAtLeast(transparentRect.left) + transparentRect.right = + (width - getRoundedCornerSizeByPosition(BOUNDS_POSITION_RIGHT)) + .coerceAtMost(transparentRect.right) + } + } + } + + private fun getRoundedCornerSizeByPosition(position: Int): Int { + val delta = displayRotation - Surface.ROTATION_0 + return when ((position + delta) % BOUNDS_POSITION_LENGTH) { + BOUNDS_POSITION_LEFT -> roundedCornerTopSize.coerceAtLeast(roundedCornerBottomSize) + BOUNDS_POSITION_TOP -> roundedCornerTopSize + BOUNDS_POSITION_RIGHT -> roundedCornerTopSize.coerceAtLeast(roundedCornerBottomSize) + BOUNDS_POSITION_BOTTOM -> roundedCornerBottomSize + else -> throw IllegalArgumentException("Incorrect position: $position") + } } private fun drawRoundedCorners(canvas: Canvas) { diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java index 2ec5f4f894f93..ae41cae10bbae 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java @@ -847,14 +847,20 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab pw.println(" mIsRoundedCornerMultipleRadius:" + mIsRoundedCornerMultipleRadius); pw.println(" mIsPrivacyDotEnabled:" + isPrivacyDotEnabled()); pw.println(" mPendingRotationChange:" + mPendingRotationChange); - pw.println(" mHwcScreenDecorationSupport:"); - if (mHwcScreenDecorationSupport == null) { - pw.println(" null"); - } else { - pw.println(" format: " + if (mHwcScreenDecorationSupport != null) { + pw.println(" mHwcScreenDecorationSupport:"); + pw.println(" format=" + PixelFormat.formatToString(mHwcScreenDecorationSupport.format)); - pw.println(" alphaInterpretation: " + pw.println(" alphaInterpretation=" + alphaInterpretationToString(mHwcScreenDecorationSupport.alphaInterpretation)); + } else { + pw.println(" mHwcScreenDecorationSupport: null"); + } + if (mScreenDecorHwcLayer != null) { + pw.println(" mScreenDecorHwcLayer:"); + pw.println(" transparentRegion=" + mScreenDecorHwcLayer.transparentRect); + } else { + pw.println(" mScreenDecorHwcLayer: null"); } pw.println(" mRoundedDefault(x,y)=(" + mRoundedDefault.x + "," + mRoundedDefault.y + ")"); pw.println(" mRoundedDefaultTop(x,y)=(" + mRoundedDefaultTop.x + "," + mRoundedDefaultTop.y diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorHwcLayerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorHwcLayerTest.kt new file mode 100644 index 0000000000000..95aa08dff379b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorHwcLayerTest.kt @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2022 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 com.android.systemui + +import android.graphics.Insets +import android.graphics.PixelFormat +import android.graphics.Rect +import android.graphics.RectF +import android.hardware.graphics.common.DisplayDecorationSupport +import android.testing.AndroidTestingRunner +import android.view.Display +import android.view.DisplayCutout +import android.view.DisplayInfo +import android.view.View +import androidx.test.filters.SmallTest +import com.android.internal.R +import com.android.systemui.util.mockito.eq +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import org.mockito.Mockito.`when` as whenever + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class ScreenDecorHwcLayerTest : SysuiTestCase() { + + @Mock private lateinit var mockDisplay: Display + @Mock private lateinit var mockRootView: View + + private val displayWidth = 100 + private val displayHeight = 200 + private val cutoutSize = 10 + private val roundedSizeTop = 15 + private val roundedSizeBottom = 20 + + private lateinit var decorHwcLayer: ScreenDecorHwcLayer + private val cutoutTop: DisplayCutout = DisplayCutout.Builder() + .setSafeInsets(Insets.of(0, cutoutSize, 0, 0)) + .setBoundingRectTop(Rect(1, 0, 2, cutoutSize)) + .build() + + private val cutoutRight: DisplayCutout = DisplayCutout.Builder() + .setSafeInsets(Insets.of(0, 0, cutoutSize, 0)) + .setBoundingRectRight(Rect(displayWidth - cutoutSize, 50, displayWidth, 52)) + .build() + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + mContext.orCreateTestableResources.addOverride( + R.array.config_displayUniqueIdArray, arrayOf()) + mContext.orCreateTestableResources.addOverride( + R.bool.config_fillMainBuiltInDisplayCutout, true) + + val decorationSupport = DisplayDecorationSupport() + decorationSupport.format = PixelFormat.R_8 + decorHwcLayer = Mockito.spy(ScreenDecorHwcLayer(mContext, decorationSupport)) + whenever(decorHwcLayer.width).thenReturn(displayWidth) + whenever(decorHwcLayer.height).thenReturn(displayHeight) + whenever(decorHwcLayer.display).thenReturn(mockDisplay) + whenever(decorHwcLayer.rootView).thenReturn(mockRootView) + whenever(mockRootView.left).thenReturn(0) + whenever(mockRootView.top).thenReturn(0) + whenever(mockRootView.right).thenReturn(displayWidth) + whenever(mockRootView.bottom).thenReturn(displayHeight) + } + + @Test + fun testTransparentRegion_noCutout_noRoundedCorner_noProtection() { + setupConfigs(null, 0, 0, RectF(), 0f) + + decorHwcLayer.calculateTransparentRect() + + assertThat(decorHwcLayer.transparentRect) + .isEqualTo(Rect(0, 0, decorHwcLayer.width, decorHwcLayer.height)) + } + + @Test + fun testTransparentRegion_onlyShortEdgeCutout() { + setupConfigs(cutoutTop, 0, 0, RectF(), 0f) + + decorHwcLayer.calculateTransparentRect() + + assertThat(decorHwcLayer.transparentRect) + .isEqualTo(Rect(0, cutoutSize, decorHwcLayer.width, decorHwcLayer.height)) + } + + @Test + fun testTransparentRegion_onlyLongEdgeCutout() { + setupConfigs(cutoutRight, 0, 0, RectF(), 0f) + + decorHwcLayer.calculateTransparentRect() + + assertThat(decorHwcLayer.transparentRect) + .isEqualTo(Rect(0, 0, decorHwcLayer.width - cutoutSize, decorHwcLayer.height)) + } + + @Test + fun testTransparentRegion_onlyRoundedCorners() { + setupConfigs(null, roundedSizeTop, roundedSizeBottom, RectF(), 0f) + + decorHwcLayer.calculateTransparentRect() + + assertThat(decorHwcLayer.transparentRect) + .isEqualTo(Rect(0, roundedSizeTop, decorHwcLayer.width, + decorHwcLayer.height - roundedSizeBottom)) + } + + @Test + fun testTransparentRegion_onlyCutoutProtection() { + setupConfigs(null, 0, 0, RectF(48f, 1f, 52f, 5f), 0.5f) + + decorHwcLayer.calculateTransparentRect() + + assertThat(decorHwcLayer.transparentRect) + .isEqualTo(Rect(0, 4, decorHwcLayer.width, decorHwcLayer.height)) + + decorHwcLayer.cameraProtectionProgress = 1f + + decorHwcLayer.calculateTransparentRect() + + assertThat(decorHwcLayer.transparentRect) + .isEqualTo(Rect(0, 5, decorHwcLayer.width, decorHwcLayer.height)) + } + + @Test + fun testTransparentRegion_hasShortEdgeCutout_hasRoundedCorner_hasCutoutProtection() { + setupConfigs(cutoutTop, roundedSizeTop, roundedSizeBottom, RectF(48f, 1f, 52f, 5f), 1f) + + decorHwcLayer.calculateTransparentRect() + + assertThat(decorHwcLayer.transparentRect) + .isEqualTo(Rect(0, 15, decorHwcLayer.width, decorHwcLayer.height - 20)) + } + + @Test + fun testTransparentRegion_hasLongEdgeCutout_hasRoundedCorner_hasCutoutProtection() { + setupConfigs(cutoutRight, roundedSizeTop, roundedSizeBottom, RectF(48f, 1f, 52f, 5f), 1f) + + decorHwcLayer.calculateTransparentRect() + + assertThat(decorHwcLayer.transparentRect) + .isEqualTo(Rect(20, 5, decorHwcLayer.width - 20, decorHwcLayer.height)) + } + + private fun setupConfigs( + cutout: DisplayCutout?, + roundedTop: Int, + roundedBottom: Int, + protectionRect: RectF, + protectionProgress: Float + ) { + whenever(mockDisplay.getDisplayInfo(eq(decorHwcLayer.displayInfo)) + ).then { + val info = it.getArgument(0) + info.displayCutout = cutout + return@then true + } + decorHwcLayer.updateRoundedCornerSize(roundedTop, roundedBottom) + decorHwcLayer.protectionRect.set(protectionRect) + decorHwcLayer.cameraProtectionProgress = protectionProgress + decorHwcLayer.updateCutout() + } +}