Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7a814173 authored by Shawn Lin's avatar Shawn Lin Committed by Automerger Merge Worker
Browse files

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

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/16961344

Change-Id: Ib2fa87114bbbea3802e7c03b003868e2c0e9db21
parents ad6aee64 fba11335
Loading
Loading
Loading
Loading
+4 −3
Original line number Diff line number Diff line
@@ -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)
+183 −1
Original line number Diff line number Diff line
@@ -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) {
+12 −6
Original line number Diff line number Diff line
@@ -847,14 +847,20 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
        pw.println("  mIsRoundedCornerMultipleRadius:" + mIsRoundedCornerMultipleRadius);
        pw.println("  mIsPrivacyDotEnabled:" + isPrivacyDotEnabled());
        pw.println("  mPendingRotationChange:" + mPendingRotationChange);
        if (mHwcScreenDecorationSupport != null) {
            pw.println("  mHwcScreenDecorationSupport:");
        if (mHwcScreenDecorationSupport == null) {
            pw.println("    null");
        } else {
            pw.println("    format: "
            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
+182 −0
Original line number Diff line number Diff line
/*
 * 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()
    }
}