Merge "ScreenDecorHwcLayer: request and report transparent regions" into tm-dev

This commit is contained in:
Shawn Lin
2022-02-24 06:41:44 +00:00
committed by Android (Google) Code Review
4 changed files with 381 additions and 10 deletions

View File

@@ -49,7 +49,7 @@ open class DisplayCutoutBaseView : View, RegionInterceptableView {
private val shouldDrawCutout: Boolean = DisplayCutout.getFillBuiltInDisplayCutout( private val shouldDrawCutout: Boolean = DisplayCutout.getFillBuiltInDisplayCutout(
context.resources, context.display?.uniqueId) context.resources, context.display?.uniqueId)
private var displayMode: Display.Mode? = null private var displayMode: Display.Mode? = null
private val location = IntArray(2) protected val location = IntArray(2)
protected var displayRotation = 0 protected var displayRotation = 0
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
@@ -65,7 +65,8 @@ open class DisplayCutoutBaseView : View, RegionInterceptableView {
@JvmField val protectionPath: Path = Path() @JvmField val protectionPath: Path = Path()
private val protectionRectOrig: RectF = RectF() private val protectionRectOrig: RectF = RectF()
private val protectionPathOrig: Path = Path() 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 private var cameraProtectionAnimator: ValueAnimator? = null
constructor(context: Context) : super(context) constructor(context: Context) : super(context)

View File

@@ -26,11 +26,22 @@ import android.graphics.PixelFormat
import android.graphics.PorterDuff import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter import android.graphics.PorterDuffColorFilter
import android.graphics.PorterDuffXfermode import android.graphics.PorterDuffXfermode
import android.graphics.Rect
import android.graphics.Region
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.hardware.graphics.common.AlphaInterpretation import android.hardware.graphics.common.AlphaInterpretation
import android.hardware.graphics.common.DisplayDecorationSupport 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.RoundedCorner
import android.view.RoundedCorners 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 * 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) class ScreenDecorHwcLayer(context: Context, displayDecorationSupport: DisplayDecorationSupport)
: DisplayCutoutBaseView(context) { : DisplayCutoutBaseView(context) {
public val colorMode: Int val colorMode: Int
private val useInvertedAlphaColor: Boolean private val useInvertedAlphaColor: Boolean
private val color: Int private val color: Int
private val bgColor: Int private val bgColor: Int
private val cornerFilter: ColorFilter private val cornerFilter: ColorFilter
private val cornerBgFilter: ColorFilter private val cornerBgFilter: ColorFilter
private val clearPaint: Paint private val clearPaint: Paint
@JvmField val transparentRect: Rect = Rect()
private val debugTransparentRegionPaint: Paint?
private val tempRect: Rect = Rect()
private var roundedCornerTopSize = 0 private var roundedCornerTopSize = 0
private var roundedCornerBottomSize = 0 private var roundedCornerBottomSize = 0
@@ -61,6 +75,10 @@ class ScreenDecorHwcLayer(context: Context, displayDecorationSupport: DisplayDec
bgColor = Color.TRANSPARENT bgColor = Color.TRANSPARENT
colorMode = ActivityInfo.COLOR_MODE_DEFAULT colorMode = ActivityInfo.COLOR_MODE_DEFAULT
useInvertedAlphaColor = false useInvertedAlphaColor = false
debugTransparentRegionPaint = Paint().apply {
color = 0x2f00ff00 // semi-transparent green
style = Paint.Style.FILL
}
} else { } else {
colorMode = ActivityInfo.COLOR_MODE_A8 colorMode = ActivityInfo.COLOR_MODE_A8
useInvertedAlphaColor = displayDecorationSupport.alphaInterpretation == useInvertedAlphaColor = displayDecorationSupport.alphaInterpretation ==
@@ -72,6 +90,7 @@ class ScreenDecorHwcLayer(context: Context, displayDecorationSupport: DisplayDec
color = Color.BLACK color = Color.BLACK
bgColor = Color.TRANSPARENT bgColor = Color.TRANSPARENT
} }
debugTransparentRegionPaint = null
} }
cornerFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) cornerFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
cornerBgFilter = PorterDuffColorFilter(bgColor, PorterDuff.Mode.SRC_OUT) cornerBgFilter = PorterDuffColorFilter(bgColor, PorterDuff.Mode.SRC_OUT)
@@ -82,6 +101,9 @@ class ScreenDecorHwcLayer(context: Context, displayDecorationSupport: DisplayDec
override fun onAttachedToWindow() { override fun onAttachedToWindow() {
super.onAttachedToWindow() super.onAttachedToWindow()
if (!DEBUG_COLOR) {
parent.requestTransparentRegion(this)
}
viewRootImpl.setDisplayDecoration(true) viewRootImpl.setDisplayDecoration(true)
if (useInvertedAlphaColor) { if (useInvertedAlphaColor) {
@@ -93,12 +115,172 @@ class ScreenDecorHwcLayer(context: Context, displayDecorationSupport: DisplayDec
} }
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
// If updating onDraw, also update gatherTransparentRegion
if (useInvertedAlphaColor) { if (useInvertedAlphaColor) {
canvas.drawColor(bgColor) canvas.drawColor(bgColor)
} }
// Cutouts are drawn in DisplayCutoutBaseView.onDraw() // Cutouts are drawn in DisplayCutoutBaseView.onDraw()
super.onDraw(canvas) super.onDraw(canvas)
drawRoundedCorners(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) { private fun drawRoundedCorners(canvas: Canvas) {

View File

@@ -847,14 +847,20 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
pw.println(" mIsRoundedCornerMultipleRadius:" + mIsRoundedCornerMultipleRadius); pw.println(" mIsRoundedCornerMultipleRadius:" + mIsRoundedCornerMultipleRadius);
pw.println(" mIsPrivacyDotEnabled:" + isPrivacyDotEnabled()); pw.println(" mIsPrivacyDotEnabled:" + isPrivacyDotEnabled());
pw.println(" mPendingRotationChange:" + mPendingRotationChange); pw.println(" mPendingRotationChange:" + mPendingRotationChange);
pw.println(" mHwcScreenDecorationSupport:"); if (mHwcScreenDecorationSupport != null) {
if (mHwcScreenDecorationSupport == null) { pw.println(" mHwcScreenDecorationSupport:");
pw.println(" null"); pw.println(" format="
} else {
pw.println(" format: "
+ PixelFormat.formatToString(mHwcScreenDecorationSupport.format)); + PixelFormat.formatToString(mHwcScreenDecorationSupport.format));
pw.println(" alphaInterpretation: " pw.println(" alphaInterpretation="
+ alphaInterpretationToString(mHwcScreenDecorationSupport.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(" mRoundedDefault(x,y)=(" + mRoundedDefault.x + "," + mRoundedDefault.y + ")");
pw.println(" mRoundedDefaultTop(x,y)=(" + mRoundedDefaultTop.x + "," + mRoundedDefaultTop.y pw.println(" mRoundedDefaultTop(x,y)=(" + mRoundedDefaultTop.x + "," + mRoundedDefaultTop.y

View File

@@ -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<String>())
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<DisplayInfo>(0)
info.displayCutout = cutout
return@then true
}
decorHwcLayer.updateRoundedCornerSize(roundedTop, roundedBottom)
decorHwcLayer.protectionRect.set(protectionRect)
decorHwcLayer.cameraProtectionProgress = protectionProgress
decorHwcLayer.updateCutout()
}
}