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

Commit eb33c9cb authored by shawnlin's avatar shawnlin
Browse files

Support hwc screen decoration layer

When the hwc supports screen decoration layer, we should:
  - create a fullscreen window and put the rounded corners and cutout in
    it.
  - set ViewRootImpl.setDisplayDecoration(true) for this window.
  - set COLOR_MODE_A8 for this window.
  - privacy dot should be put in windows with RGB color mode and only
    the window that the dot shows in will be shown. So there will be at
    most 2 windows(the fullscreen hwc window and the normal window that
    the visible dot is in) shown at the same time.

When the hwc not supports screen decoration layer, fallback to the
orignal 2 windows design.

Added a new class DisplayCutoutBaseView and move the common actions of a
cutout view from DisplayCutoutView to it:
  - Draw cutouts.
  - Handle camera protection.
  - Intercepts touches on cutout areas.

Handle the case when the support of hwc screen decorations is changed
due to display change.

Bug: 209498721
Test: atest ScreenDecorationsTest DisplayCutoutBaseViewTest
Change-Id: I286d5d368132381ad56798b794cd18f4ea5d03c5
parent 52855a06
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@
        android:layout_height="12dp"
        android:layout_gravity="left|bottom"
        android:tint="#ff000000"
        android:visibility="gone"
        android:src="@drawable/rounded_corner_bottom"/>

    <ImageView
@@ -32,6 +33,7 @@
        android:layout_width="12dp"
        android:layout_height="12dp"
        android:tint="#ff000000"
        android:visibility="gone"
        android:layout_gravity="right|bottom"
        android:src="@drawable/rounded_corner_bottom"/>

+2 −0
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@
        android:layout_height="12dp"
        android:layout_gravity="left|top"
        android:tint="#ff000000"
        android:visibility="gone"
        android:src="@drawable/rounded_corner_top"/>

    <ImageView
@@ -32,6 +33,7 @@
        android:layout_width="12dp"
        android:layout_height="12dp"
        android:tint="#ff000000"
        android:visibility="gone"
        android:layout_gravity="right|top"
        android:src="@drawable/rounded_corner_top"/>

+21 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ 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.
  -->
<com.android.systemui.RegionInterceptingFrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/screen_decor_hwc_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
+276 −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.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.annotation.Dimension
import android.content.Context
import android.graphics.Canvas
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.Path
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.Region
import android.util.AttributeSet
import android.view.Display
import android.view.DisplayCutout
import android.view.DisplayInfo
import android.view.Surface
import android.view.View
import androidx.annotation.VisibleForTesting
import com.android.systemui.RegionInterceptingFrameLayout.RegionInterceptableView
import com.android.systemui.animation.Interpolators

/**
 *  A class that handles common actions of display cutout view.
 *  - Draws cutouts.
 *  - Handles camera protection.
 *  - Intercepts touches on cutout areas.
 */
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 var displayRotation = 0

    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
    @JvmField val displayInfo = DisplayInfo()
    @JvmField protected var pendingRotationChange = false
    @JvmField protected val paint = Paint()
    @JvmField protected val cutoutPath = Path()

    @JvmField protected var showProtection = false
    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
    @JvmField val protectionRect: RectF = RectF()
    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
    @JvmField val protectionPath: Path = Path()
    private val protectionRectOrig: RectF = RectF()
    private val protectionPathOrig: Path = Path()
    private var cameraProtectionProgress: Float = HIDDEN_CAMERA_PROTECTION_SCALE
    private var cameraProtectionAnimator: ValueAnimator? = null

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
            : super(context, attrs, defStyleAttr)

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        updateCutout()
    }

    fun onDisplayChanged(displayId: Int) {
        val oldMode: Display.Mode? = displayMode
        displayMode = display.mode

        // Skip if display mode or cutout hasn't changed.
        if (!displayModeChanged(oldMode, displayMode) &&
                display.cutout == displayInfo.displayCutout) {
            return
        }
        if (displayId == display.displayId) {
            updateCutout()
            updateProtectionBoundingPath()
        }
    }

    open fun updateRotation(rotation: Int) {
        displayRotation = rotation
        updateCutout()
        updateProtectionBoundingPath()
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
    public override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (!shouldDrawCutout) {
            return
        }
        canvas.save()
        getLocationOnScreen(location)
        canvas.translate(-location[0].toFloat(), -location[1].toFloat())

        drawCutouts(canvas)
        drawCutoutProtection(canvas)
        canvas.restore()
    }

    override fun shouldInterceptTouch(): Boolean {
        return displayInfo.displayCutout != null && visibility == VISIBLE && shouldDrawCutout
    }

    override fun getInterceptRegion(): Region? {
        displayInfo.displayCutout ?: return null

        val cutoutBounds: Region = rectsToRegion(displayInfo.displayCutout?.boundingRects)
        // Transform to window's coordinate space
        rootView.getLocationOnScreen(location)
        cutoutBounds.translate(-location[0], -location[1])

        // Intersect with window's frame
        cutoutBounds.op(rootView.left, rootView.top, rootView.right, rootView.bottom,
                Region.Op.INTERSECT)
        return cutoutBounds
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
    open fun updateCutout() {
        if (pendingRotationChange) {
            return
        }
        cutoutPath.reset()
        display.getDisplayInfo(displayInfo)
        displayInfo.displayCutout?.cutoutPath?.let { path -> cutoutPath.set(path) }
        invalidate()
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
    open fun drawCutouts(canvas: Canvas) {
        displayInfo.displayCutout?.cutoutPath ?: return
        canvas.drawPath(cutoutPath, paint)
    }

    protected open fun drawCutoutProtection(canvas: Canvas) {
        if (cameraProtectionProgress > HIDDEN_CAMERA_PROTECTION_SCALE &&
                !protectionRect.isEmpty) {
            canvas.scale(cameraProtectionProgress, cameraProtectionProgress,
                    protectionRect.centerX(), protectionRect.centerY())
            canvas.drawPath(protectionPath, paint)
        }
    }

    /**
     * Converts a set of [Rect]s into a [Region]
     */
    fun rectsToRegion(rects: List<Rect?>?): Region {
        val result = Region.obtain()
        if (rects != null) {
            for (r in rects) {
                if (r != null && !r.isEmpty) {
                    result.op(r, Region.Op.UNION)
                }
            }
        }
        return result
    }

    open fun enableShowProtection(show: Boolean) {
        if (showProtection == show) {
            return
        }
        showProtection = show
        updateProtectionBoundingPath()
        // Delay the relayout until the end of the animation when hiding the cutout,
        // otherwise we'd clip it.
        if (showProtection) {
            requestLayout()
        }
        cameraProtectionAnimator?.cancel()
        cameraProtectionAnimator = ValueAnimator.ofFloat(cameraProtectionProgress,
                if (showProtection) 1.0f else HIDDEN_CAMERA_PROTECTION_SCALE).setDuration(750)
        cameraProtectionAnimator?.interpolator = Interpolators.DECELERATE_QUINT
        cameraProtectionAnimator?.addUpdateListener(ValueAnimator.AnimatorUpdateListener {
            animation: ValueAnimator ->
            cameraProtectionProgress = animation.animatedValue as Float
            invalidate()
        })
        cameraProtectionAnimator?.addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator) {
                cameraProtectionAnimator = null
                if (!showProtection) {
                    requestLayout()
                }
            }
        })
        cameraProtectionAnimator?.start()
    }

    open fun setProtection(path: Path, pathBounds: Rect) {
        protectionPathOrig.reset()
        protectionPathOrig.set(path)
        protectionPath.reset()
        protectionRectOrig.setEmpty()
        protectionRectOrig.set(pathBounds)
        protectionRect.setEmpty()
    }

    protected open fun updateProtectionBoundingPath() {
        if (pendingRotationChange) {
            return
        }
        val lw: Int = displayInfo.logicalWidth
        val lh: Int = displayInfo.logicalHeight
        val flipped = (displayInfo.rotation == Surface.ROTATION_90 ||
                displayInfo.rotation == Surface.ROTATION_270)
        val dw = if (flipped) lh else lw
        val dh = if (flipped) lw else lh
        val m = Matrix()
        transformPhysicalToLogicalCoordinates(displayInfo.rotation, dw, dh, m)
        if (!protectionPathOrig.isEmpty) {
            // Reset the protection path so we don't aggregate rotations
            protectionPath.set(protectionPathOrig)
            protectionPath.transform(m)
            m.mapRect(protectionRect, protectionRectOrig)
        }
    }

    private fun displayModeChanged(oldMode: Display.Mode?, newMode: Display.Mode?): Boolean {
        if (oldMode == null) {
            return true
        }

        // We purposely ignore refresh rate and id changes here, because we don't need to
        // invalidate for those, and they can trigger the refresh rate to increase
        return oldMode?.physicalHeight != newMode?.physicalHeight ||
                oldMode?.physicalWidth != newMode?.physicalWidth
    }

    companion object {
        private const val HIDDEN_CAMERA_PROTECTION_SCALE = 0.5f

        @JvmStatic protected fun transformPhysicalToLogicalCoordinates(
            @Surface.Rotation rotation: Int,
            @Dimension physicalWidth: Int,
            @Dimension physicalHeight: Int,
            out: Matrix
        ) {
            when (rotation) {
                Surface.ROTATION_0 -> out.reset()
                Surface.ROTATION_90 -> {
                    out.setRotate(270f)
                    out.postTranslate(0f, physicalWidth.toFloat())
                }
                Surface.ROTATION_180 -> {
                    out.setRotate(180f)
                    out.postTranslate(physicalWidth.toFloat(), physicalHeight.toFloat())
                }
                Surface.ROTATION_270 -> {
                    out.setRotate(90f)
                    out.postTranslate(physicalHeight.toFloat(), 0f)
                }
                else -> throw IllegalArgumentException("Unknown rotation: $rotation")
            }
        }
    }
}
 No newline at end of file
+195 −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.content.Context
import android.content.pm.ActivityInfo
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.PorterDuffXfermode
import android.graphics.drawable.Drawable
import android.hardware.graphics.common.AlphaInterpretation
import android.hardware.graphics.common.DisplayDecorationSupport
import android.view.RoundedCorner
import android.view.RoundedCorners

/**
 * When the HWC of the device supports Composition.DISPLAY_DECORATON, we use this layer to draw
 * screen decorations.
 */
class ScreenDecorHwcLayer(context: Context, displayDecorationSupport: DisplayDecorationSupport)
    : DisplayCutoutBaseView(context) {
    public 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

    private var roundedCornerTopSize = 0
    private var roundedCornerBottomSize = 0
    private var roundedCornerDrawableTop: Drawable? = null
    private var roundedCornerDrawableBottom: Drawable? = null

    init {
        if (displayDecorationSupport.format != PixelFormat.R_8) {
            throw IllegalArgumentException("Attempting to use unsupported mode " +
                    "${PixelFormat.formatToString(displayDecorationSupport.format)}")
        }
        if (DEBUG_COLOR) {
            color = Color.GREEN
            bgColor = Color.TRANSPARENT
            colorMode = ActivityInfo.COLOR_MODE_DEFAULT
            useInvertedAlphaColor = false
        } else {
            colorMode = ActivityInfo.COLOR_MODE_A8
            useInvertedAlphaColor = displayDecorationSupport.alphaInterpretation ==
                    AlphaInterpretation.COVERAGE
            if (useInvertedAlphaColor) {
                color = Color.TRANSPARENT
                bgColor = Color.BLACK
            } else {
                color = Color.BLACK
                bgColor = Color.TRANSPARENT
            }
        }
        cornerFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
        cornerBgFilter = PorterDuffColorFilter(bgColor, PorterDuff.Mode.SRC_OUT)

        clearPaint = Paint()
        clearPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        viewRootImpl.setDisplayDecoration(true)

        if (useInvertedAlphaColor) {
            paint.set(clearPaint)
        } else {
            paint.color = color
            paint.style = Paint.Style.FILL
        }
    }

    override fun onDraw(canvas: Canvas) {
        if (useInvertedAlphaColor) {
            canvas.drawColor(bgColor)
        }
        // Cutouts are drawn in DisplayCutoutBaseView.onDraw()
        super.onDraw(canvas)
        drawRoundedCorners(canvas)
    }

    private fun drawRoundedCorners(canvas: Canvas) {
        if (roundedCornerTopSize == 0 && roundedCornerBottomSize == 0) {
            return
        }
        var degree: Int
        for (i in RoundedCorner.POSITION_TOP_LEFT
                until RoundedCorners.ROUNDED_CORNER_POSITION_LENGTH) {
            canvas.save()
            degree = getRoundedCornerRotationDegree(90 * i)
            canvas.rotate(degree.toFloat())
            canvas.translate(
                    getRoundedCornerTranslationX(degree).toFloat(),
                    getRoundedCornerTranslationY(degree).toFloat())
            if (i == RoundedCorner.POSITION_TOP_LEFT || i == RoundedCorner.POSITION_TOP_RIGHT) {
                drawRoundedCorner(canvas, roundedCornerDrawableTop, roundedCornerTopSize)
            } else {
                drawRoundedCorner(canvas, roundedCornerDrawableBottom, roundedCornerBottomSize)
            }
            canvas.restore()
        }
    }

    private fun drawRoundedCorner(canvas: Canvas, drawable: Drawable?, size: Int) {
        if (useInvertedAlphaColor) {
            canvas.drawRect(0f, 0f, size.toFloat(), size.toFloat(), clearPaint)
            drawable?.colorFilter = cornerBgFilter
        } else {
            drawable?.colorFilter = cornerFilter
        }
        drawable?.draw(canvas)
        // Clear color filter when we are done with drawing.
        drawable?.clearColorFilter()
    }

    private fun getRoundedCornerRotationDegree(defaultDegree: Int): Int {
        return (defaultDegree - 90 * displayRotation + 360) % 360
    }

    private fun getRoundedCornerTranslationX(degree: Int): Int {
        return when (degree) {
            0, 90 -> 0
            180 -> -width
            270 -> -height
            else -> throw IllegalArgumentException("Incorrect degree: $degree")
        }
    }

    private fun getRoundedCornerTranslationY(degree: Int): Int {
        return when (degree) {
            0, 270 -> 0
            90 -> -width
            180 -> -height
            else -> throw IllegalArgumentException("Incorrect degree: $degree")
        }
    }

    /**
     * Update the rounded corner drawables.
     */
    fun updateRoundedCornerDrawable(top: Drawable, bottom: Drawable) {
        roundedCornerDrawableTop = top
        roundedCornerDrawableBottom = bottom
        updateRoundedCornerDrawableBounds()
        invalidate()
    }

    /**
     * Update the rounded corner size.
     */
    fun updateRoundedCornerSize(top: Int, bottom: Int) {
        roundedCornerTopSize = top
        roundedCornerBottomSize = bottom
        updateRoundedCornerDrawableBounds()
        invalidate()
    }

    private fun updateRoundedCornerDrawableBounds() {
        if (roundedCornerDrawableTop != null) {
            roundedCornerDrawableTop?.setBounds(0, 0, roundedCornerTopSize,
                    roundedCornerTopSize)
        }
        if (roundedCornerDrawableBottom != null) {
            roundedCornerDrawableBottom?.setBounds(0, 0, roundedCornerBottomSize,
                    roundedCornerBottomSize)
        }
        invalidate()
    }

    companion object {
        private val DEBUG_COLOR = ScreenDecorations.DEBUG_COLOR
    }
}
Loading