Merge "Support hwc screen decoration layer"
This commit is contained in:
@@ -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"/>
|
||||
|
||||
|
||||
@@ -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
packages/SystemUI/res/layout/screen_decor_hwc_layer.xml
Normal file
21
packages/SystemUI/res/layout/screen_decor_hwc_layer.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?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" />
|
||||
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
@@ -14,24 +14,17 @@
|
||||
|
||||
package com.android.systemui;
|
||||
|
||||
import static android.view.Display.DEFAULT_DISPLAY;
|
||||
import static android.view.DisplayCutout.BOUNDS_POSITION_BOTTOM;
|
||||
import static android.view.DisplayCutout.BOUNDS_POSITION_LEFT;
|
||||
import static android.view.DisplayCutout.BOUNDS_POSITION_LENGTH;
|
||||
import static android.view.DisplayCutout.BOUNDS_POSITION_RIGHT;
|
||||
import static android.view.DisplayCutout.BOUNDS_POSITION_TOP;
|
||||
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 android.view.ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.Dimension;
|
||||
import android.annotation.IdRes;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
@@ -40,11 +33,11 @@ import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
@@ -52,10 +45,10 @@ import android.graphics.Path;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Region;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.hardware.graphics.common.AlphaInterpretation;
|
||||
import android.hardware.graphics.common.DisplayDecorationSupport;
|
||||
import android.os.Handler;
|
||||
import android.os.SystemProperties;
|
||||
import android.os.UserHandle;
|
||||
@@ -63,14 +56,11 @@ import android.provider.Settings.Secure;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.DisplayUtils;
|
||||
import android.util.Log;
|
||||
import android.view.Display;
|
||||
import android.view.DisplayCutout;
|
||||
import android.view.DisplayCutout.BoundsPosition;
|
||||
import android.view.DisplayInfo;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.RoundedCorners;
|
||||
import android.view.Surface;
|
||||
import android.view.View;
|
||||
import android.view.View.OnLayoutChangeListener;
|
||||
import android.view.ViewGroup;
|
||||
@@ -83,8 +73,6 @@ import android.widget.ImageView;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.android.internal.util.Preconditions;
|
||||
import com.android.systemui.RegionInterceptingFrameLayout.RegionInterceptableView;
|
||||
import com.android.systemui.animation.Interpolators;
|
||||
import com.android.systemui.broadcast.BroadcastDispatcher;
|
||||
import com.android.systemui.dagger.SysUISingleton;
|
||||
import com.android.systemui.dagger.qualifiers.Main;
|
||||
@@ -106,6 +94,7 @@ import java.io.FileDescriptor;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -117,7 +106,7 @@ import kotlin.Pair;
|
||||
* for antialiasing and emulation purposes.
|
||||
*/
|
||||
@SysUISingleton
|
||||
public class ScreenDecorations extends CoreStartable implements Tunable , Dumpable{
|
||||
public class ScreenDecorations extends CoreStartable implements Tunable , Dumpable {
|
||||
private static final boolean DEBUG = false;
|
||||
private static final String TAG = "ScreenDecorations";
|
||||
|
||||
@@ -129,7 +118,7 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
private static final boolean DEBUG_SCREENSHOT_ROUNDED_CORNERS =
|
||||
SystemProperties.getBoolean("debug.screenshot_rounded_corners", false);
|
||||
private static final boolean VERBOSE = false;
|
||||
private static final boolean DEBUG_COLOR = DEBUG_SCREENSHOT_ROUNDED_CORNERS;
|
||||
static final boolean DEBUG_COLOR = DEBUG_SCREENSHOT_ROUNDED_CORNERS;
|
||||
|
||||
private DisplayManager mDisplayManager;
|
||||
@VisibleForTesting
|
||||
@@ -138,7 +127,8 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
private final Executor mMainExecutor;
|
||||
private final TunerService mTunerService;
|
||||
private final SecureSettings mSecureSettings;
|
||||
private DisplayManager.DisplayListener mDisplayListener;
|
||||
@VisibleForTesting
|
||||
DisplayManager.DisplayListener mDisplayListener;
|
||||
private CameraAvailabilityListener mCameraListener;
|
||||
private final UserTracker mUserTracker;
|
||||
private final PrivacyDotViewController mDotViewController;
|
||||
@@ -158,23 +148,36 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
protected OverlayWindow[] mOverlays = null;
|
||||
@Nullable
|
||||
private DisplayCutoutView[] mCutoutViews;
|
||||
@VisibleForTesting
|
||||
ViewGroup mScreenDecorHwcWindow;
|
||||
@VisibleForTesting
|
||||
ScreenDecorHwcLayer mScreenDecorHwcLayer;
|
||||
private float mDensity;
|
||||
private WindowManager mWindowManager;
|
||||
private int mRotation;
|
||||
private SettingObserver mColorInversionSetting;
|
||||
private DelayableExecutor mExecutor;
|
||||
private Handler mHandler;
|
||||
private boolean mPendingRotationChange;
|
||||
boolean mPendingRotationChange;
|
||||
private boolean mIsRoundedCornerMultipleRadius;
|
||||
private Drawable mRoundedCornerDrawable;
|
||||
private Drawable mRoundedCornerDrawableTop;
|
||||
private Drawable mRoundedCornerDrawableBottom;
|
||||
private String mDisplayUniqueId;
|
||||
@VisibleForTesting
|
||||
String mDisplayUniqueId;
|
||||
private int mTintColor = Color.BLACK;
|
||||
@VisibleForTesting
|
||||
protected DisplayDecorationSupport mHwcScreenDecorationSupport;
|
||||
|
||||
private CameraAvailabilityListener.CameraTransitionCallback mCameraTransitionCallback =
|
||||
new CameraAvailabilityListener.CameraTransitionCallback() {
|
||||
@Override
|
||||
public void onApplyCameraProtection(@NonNull Path protectionPath, @NonNull Rect bounds) {
|
||||
if (mScreenDecorHwcLayer != null) {
|
||||
mScreenDecorHwcLayer.setProtection(protectionPath, bounds);
|
||||
mScreenDecorHwcLayer.enableShowProtection(true);
|
||||
return;
|
||||
}
|
||||
if (mCutoutViews == null) {
|
||||
Log.w(TAG, "DisplayCutoutView do not initialized");
|
||||
return;
|
||||
@@ -184,13 +187,17 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
// Check Null since not all mCutoutViews[pos] be inflated at the meanwhile
|
||||
if (dcv != null) {
|
||||
dcv.setProtection(protectionPath, bounds);
|
||||
dcv.setShowProtection(true);
|
||||
dcv.enableShowProtection(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHideCameraProtection() {
|
||||
if (mScreenDecorHwcLayer != null) {
|
||||
mScreenDecorHwcLayer.enableShowProtection(false);
|
||||
return;
|
||||
}
|
||||
if (mCutoutViews == null) {
|
||||
Log.w(TAG, "DisplayCutoutView do not initialized");
|
||||
return;
|
||||
@@ -199,27 +206,59 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
for (DisplayCutoutView dcv : mCutoutViews) {
|
||||
// Check Null since not all mCutoutViews[pos] be inflated at the meanwhile
|
||||
if (dcv != null) {
|
||||
dcv.setShowProtection(false);
|
||||
dcv.enableShowProtection(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a set of {@link Rect}s into a {@link Region}
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public static Region rectsToRegion(List<Rect> rects) {
|
||||
Region result = Region.obtain();
|
||||
if (rects != null) {
|
||||
for (Rect r : rects) {
|
||||
if (r != null && !r.isEmpty()) {
|
||||
result.op(r, Region.Op.UNION);
|
||||
}
|
||||
private PrivacyDotViewController.ShowingListener mPrivacyDotShowingListener =
|
||||
new PrivacyDotViewController.ShowingListener() {
|
||||
@Override
|
||||
public void onPrivacyDotShown(@Nullable View v) {
|
||||
// We don't need to control the window visibility when the hwc doesn't support screen
|
||||
// decoration since the overlay windows are always visible in this case.
|
||||
if (mHwcScreenDecorationSupport == null || v == null) {
|
||||
return;
|
||||
}
|
||||
mExecutor.execute(() -> {
|
||||
for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
|
||||
if (mOverlays[i] == null) {
|
||||
continue;
|
||||
}
|
||||
final ViewGroup overlayView = mOverlays[i].getRootView();
|
||||
if (overlayView.findViewById(v.getId()) != null) {
|
||||
overlayView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return result;
|
||||
|
||||
@Override
|
||||
public void onPrivacyDotHidden(@Nullable View v) {
|
||||
// We don't need to control the window visibility when the hwc doesn't support screen
|
||||
// decoration since the overlay windows are always visible in this case.
|
||||
if (mHwcScreenDecorationSupport == null || v == null) {
|
||||
return;
|
||||
}
|
||||
mExecutor.execute(() -> {
|
||||
for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
|
||||
if (mOverlays[i] == null) {
|
||||
continue;
|
||||
}
|
||||
final ViewGroup overlayView = mOverlays[i].getRootView();
|
||||
if (overlayView.findViewById(v.getId()) != null) {
|
||||
overlayView.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private static boolean eq(DisplayDecorationSupport a, DisplayDecorationSupport b) {
|
||||
if (a == null) return (b == null);
|
||||
if (b == null) return false;
|
||||
return a.format == b.format && a.alphaInterpretation == b.alphaInterpretation;
|
||||
}
|
||||
|
||||
@Inject
|
||||
@@ -241,6 +280,7 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
mDotViewController = dotViewController;
|
||||
mThreadFactory = threadFactory;
|
||||
mDotFactory = dotFactory;
|
||||
dotViewController.setShowingListener(mPrivacyDotShowingListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -265,6 +305,7 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
mIsRoundedCornerMultipleRadius = isRoundedCornerMultipleRadius(mContext, mDisplayUniqueId);
|
||||
mWindowManager = mContext.getSystemService(WindowManager.class);
|
||||
mDisplayManager = mContext.getSystemService(DisplayManager.class);
|
||||
mHwcScreenDecorationSupport = mContext.getDisplay().getDisplayDecorationSupport();
|
||||
updateRoundedCornerDrawable();
|
||||
updateRoundedCornerRadii();
|
||||
setupDecorations();
|
||||
@@ -305,15 +346,36 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
new RestartingPreDrawListener(overlayView, i, newRotation));
|
||||
}
|
||||
}
|
||||
|
||||
if (mScreenDecorHwcWindow != null) {
|
||||
mScreenDecorHwcWindow.getViewTreeObserver().addOnPreDrawListener(
|
||||
new RestartingPreDrawListener(
|
||||
mScreenDecorHwcWindow,
|
||||
-1, // Pass -1 for views with no specific position.
|
||||
newRotation));
|
||||
}
|
||||
}
|
||||
|
||||
final String newUniqueId = mContext.getDisplay().getUniqueId();
|
||||
if ((newUniqueId != null && !newUniqueId.equals(mDisplayUniqueId))
|
||||
|| (mDisplayUniqueId != null && !mDisplayUniqueId.equals(newUniqueId))) {
|
||||
if (!Objects.equals(newUniqueId, mDisplayUniqueId)) {
|
||||
mDisplayUniqueId = newUniqueId;
|
||||
mIsRoundedCornerMultipleRadius =
|
||||
isRoundedCornerMultipleRadius(mContext, mDisplayUniqueId);
|
||||
final DisplayDecorationSupport newScreenDecorationSupport =
|
||||
mContext.getDisplay().getDisplayDecorationSupport();
|
||||
// When the value of mSupportHwcScreenDecoration is changed, re-setup the whole
|
||||
// screen decoration.
|
||||
if (!eq(newScreenDecorationSupport, mHwcScreenDecorationSupport)) {
|
||||
mHwcScreenDecorationSupport = newScreenDecorationSupport;
|
||||
removeAllOverlays();
|
||||
setupDecorations();
|
||||
return;
|
||||
}
|
||||
updateRoundedCornerDrawable();
|
||||
}
|
||||
if (mScreenDecorHwcLayer != null) {
|
||||
mScreenDecorHwcLayer.onDisplayChanged(displayId);
|
||||
}
|
||||
updateOrientation();
|
||||
}
|
||||
};
|
||||
@@ -359,6 +421,11 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
List<DecorProvider> decorProviders = mDotFactory.getProviders();
|
||||
|
||||
if (hasRoundedCorners() || shouldDrawCutout() || !decorProviders.isEmpty()) {
|
||||
if (mHwcScreenDecorationSupport != null) {
|
||||
createHwcOverlay();
|
||||
} else {
|
||||
removeHwcOverlay();
|
||||
}
|
||||
final DisplayCutout cutout = getCutout();
|
||||
for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
|
||||
if (shouldShowCutout(i, cutout) || shouldShowRoundedCorner(i, cutout)
|
||||
@@ -383,14 +450,15 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
}
|
||||
} else {
|
||||
removeAllOverlays();
|
||||
removeHwcOverlay();
|
||||
}
|
||||
|
||||
if (hasOverlays()) {
|
||||
if (hasOverlays() || hasHwcOverlay()) {
|
||||
if (mIsRegistered) {
|
||||
return;
|
||||
}
|
||||
DisplayMetrics metrics = new DisplayMetrics();
|
||||
mDisplayManager.getDisplay(DEFAULT_DISPLAY).getMetrics(metrics);
|
||||
mContext.getDisplay().getMetrics(metrics);
|
||||
mDensity = metrics.density;
|
||||
|
||||
mMainExecutor.execute(() -> mTunerService.addTunable(this, SIZE));
|
||||
@@ -475,25 +543,27 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
mOverlays = new OverlayWindow[BOUNDS_POSITION_LENGTH];
|
||||
}
|
||||
|
||||
if (mCutoutViews == null) {
|
||||
mCutoutViews = new DisplayCutoutView[BOUNDS_POSITION_LENGTH];
|
||||
}
|
||||
|
||||
if (mOverlays[pos] != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mOverlays[pos] = overlayForPosition(pos, decorProviders);
|
||||
|
||||
mCutoutViews[pos] = new DisplayCutoutView(mContext, pos, this);
|
||||
mOverlays[pos].getRootView().addView(mCutoutViews[pos]);
|
||||
|
||||
final ViewGroup overlayView = mOverlays[pos].getRootView();
|
||||
overlayView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
||||
overlayView.setAlpha(0);
|
||||
overlayView.setForceDarkAllowed(false);
|
||||
|
||||
updateView(pos, cutout);
|
||||
// Only show cutout and rounded corners in mOverlays when hwc don't support screen
|
||||
// decoration.
|
||||
if (mHwcScreenDecorationSupport == null) {
|
||||
if (mCutoutViews == null) {
|
||||
mCutoutViews = new DisplayCutoutView[BOUNDS_POSITION_LENGTH];
|
||||
}
|
||||
mCutoutViews[pos] = new DisplayCutoutView(mContext, pos);
|
||||
mCutoutViews[pos].setColor(mTintColor);
|
||||
overlayView.addView(mCutoutViews[pos]);
|
||||
updateView(pos, cutout);
|
||||
}
|
||||
|
||||
mWindowManager.addView(overlayView, getWindowLayoutParams(pos));
|
||||
|
||||
@@ -509,8 +579,37 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
}
|
||||
});
|
||||
|
||||
mOverlays[pos].getRootView().getViewTreeObserver().addOnPreDrawListener(
|
||||
new ValidatingPreDrawListener(mOverlays[pos].getRootView()));
|
||||
overlayView.getRootView().getViewTreeObserver().addOnPreDrawListener(
|
||||
new ValidatingPreDrawListener(overlayView.getRootView()));
|
||||
}
|
||||
|
||||
private boolean hasHwcOverlay() {
|
||||
return mScreenDecorHwcWindow != null;
|
||||
}
|
||||
|
||||
private void removeHwcOverlay() {
|
||||
if (mScreenDecorHwcWindow == null) {
|
||||
return;
|
||||
}
|
||||
mWindowManager.removeViewImmediate(mScreenDecorHwcWindow);
|
||||
mScreenDecorHwcWindow = null;
|
||||
mScreenDecorHwcLayer = null;
|
||||
}
|
||||
|
||||
private void createHwcOverlay() {
|
||||
if (mScreenDecorHwcWindow != null) {
|
||||
return;
|
||||
}
|
||||
mScreenDecorHwcWindow = (ViewGroup) LayoutInflater.from(mContext).inflate(
|
||||
R.layout.screen_decor_hwc_layer, null);
|
||||
mScreenDecorHwcLayer = new ScreenDecorHwcLayer(mContext, mHwcScreenDecorationSupport);
|
||||
mScreenDecorHwcWindow.addView(mScreenDecorHwcLayer, new FrameLayout.LayoutParams(
|
||||
MATCH_PARENT, MATCH_PARENT, Gravity.TOP | Gravity.START));
|
||||
mWindowManager.addView(mScreenDecorHwcWindow, getHwcWindowLayoutParams());
|
||||
updateRoundedCornerSize(mRoundedDefault, mRoundedDefaultTop, mRoundedDefaultBottom);
|
||||
updateRoundedCornerImageView();
|
||||
mScreenDecorHwcWindow.getViewTreeObserver().addOnPreDrawListener(
|
||||
new ValidatingPreDrawListener(mScreenDecorHwcWindow));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -523,12 +622,18 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
decorProviders.forEach(provider -> {
|
||||
removeOverlayView(provider.getViewId());
|
||||
currentOverlay.addDecorProvider(provider, mRotation);
|
||||
// If the hwc supports screen decoration and privacy dot is enabled, it means there will
|
||||
// be only privacy dot in mOverlay. So set the initial visibility of mOverlays to
|
||||
// INVISIBLE and will only set it to VISIBLE when the privacy dot is showing.
|
||||
if (mHwcScreenDecorationSupport != null) {
|
||||
currentOverlay.getRootView().setVisibility(View.INVISIBLE);
|
||||
}
|
||||
});
|
||||
return currentOverlay;
|
||||
}
|
||||
|
||||
private void updateView(@BoundsPosition int pos, @Nullable DisplayCutout cutout) {
|
||||
if (mOverlays == null || mOverlays[pos] == null) {
|
||||
if (mOverlays == null || mOverlays[pos] == null || mHwcScreenDecorationSupport != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -540,15 +645,34 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
|
||||
// update cutout view rotation
|
||||
if (mCutoutViews != null && mCutoutViews[pos] != null) {
|
||||
mCutoutViews[pos].setRotation(mRotation);
|
||||
mCutoutViews[pos].updateRotation(mRotation);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
WindowManager.LayoutParams getWindowLayoutParams(@BoundsPosition int pos) {
|
||||
final WindowManager.LayoutParams lp = getWindowLayoutBaseParams();
|
||||
lp.width = getWidthLayoutParamByPos(pos);
|
||||
lp.height = getHeightLayoutParamByPos(pos);
|
||||
lp.setTitle(getWindowTitleByPos(pos));
|
||||
lp.gravity = getOverlayWindowGravity(pos);
|
||||
return lp;
|
||||
}
|
||||
|
||||
private WindowManager.LayoutParams getHwcWindowLayoutParams() {
|
||||
final WindowManager.LayoutParams lp = getWindowLayoutBaseParams();
|
||||
lp.width = MATCH_PARENT;
|
||||
lp.height = MATCH_PARENT;
|
||||
lp.setTitle("ScreenDecorHwcOverlay");
|
||||
lp.gravity = Gravity.TOP | Gravity.START;
|
||||
if (!DEBUG_COLOR) {
|
||||
lp.setColorMode(ActivityInfo.COLOR_MODE_A8);
|
||||
}
|
||||
return lp;
|
||||
}
|
||||
|
||||
private WindowManager.LayoutParams getWindowLayoutBaseParams() {
|
||||
final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
|
||||
getWidthLayoutParamByPos(pos),
|
||||
getHeightLayoutParamByPos(pos),
|
||||
WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
|
||||
@@ -566,8 +690,6 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
|
||||
}
|
||||
|
||||
lp.setTitle(getWindowTitleByPos(pos));
|
||||
lp.gravity = getOverlayWindowGravity(pos);
|
||||
lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
|
||||
lp.setFitInsetsTypes(0 /* types */);
|
||||
lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC;
|
||||
@@ -648,15 +770,19 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
};
|
||||
|
||||
private void updateColorInversion(int colorsInvertedValue) {
|
||||
int tint = colorsInvertedValue != 0 ? Color.WHITE : Color.BLACK;
|
||||
mTintColor = colorsInvertedValue != 0 ? Color.WHITE : Color.BLACK;
|
||||
if (DEBUG_COLOR) {
|
||||
tint = Color.RED;
|
||||
mTintColor = Color.RED;
|
||||
}
|
||||
ColorStateList tintList = ColorStateList.valueOf(tint);
|
||||
|
||||
if (mOverlays == null) {
|
||||
// When the hwc supports screen decorations, the layer will use the A8 color mode which
|
||||
// won't be affected by the color inversion. If the composition goes the client composition
|
||||
// route, the color inversion will be handled by the RenderEngine.
|
||||
if (mOverlays == null || mHwcScreenDecorationSupport != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ColorStateList tintList = ColorStateList.valueOf(mTintColor);
|
||||
for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
|
||||
if (mOverlays[i] == null) {
|
||||
continue;
|
||||
@@ -676,7 +802,7 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
if (child instanceof ImageView) {
|
||||
((ImageView) child).setImageTintList(tintList);
|
||||
} else if (child instanceof DisplayCutoutView) {
|
||||
((DisplayCutoutView) child).setColor(tint);
|
||||
((DisplayCutoutView) child).setColor(mTintColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -688,6 +814,7 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
Log.i(TAG, "ScreenDecorations is disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
mExecutor.execute(() -> {
|
||||
int oldRotation = mRotation;
|
||||
mPendingRotationChange = false;
|
||||
@@ -705,6 +832,14 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
});
|
||||
}
|
||||
|
||||
private static String alphaInterpretationToString(int alpha) {
|
||||
switch (alpha) {
|
||||
case AlphaInterpretation.COVERAGE: return "COVERAGE";
|
||||
case AlphaInterpretation.MASK: return "MASK";
|
||||
default: return "Unknown: " + alpha;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
|
||||
pw.println("ScreenDecorations state:");
|
||||
@@ -712,6 +847,15 @@ 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: "
|
||||
+ PixelFormat.formatToString(mHwcScreenDecorationSupport.format));
|
||||
pw.println(" alphaInterpretation: "
|
||||
+ alphaInterpretationToString(mHwcScreenDecorationSupport.alphaInterpretation));
|
||||
}
|
||||
pw.println(" mRoundedDefault(x,y)=(" + mRoundedDefault.x + "," + mRoundedDefault.y + ")");
|
||||
pw.println(" mRoundedDefaultTop(x,y)=(" + mRoundedDefaultTop.x + "," + mRoundedDefaultTop.y
|
||||
+ ")");
|
||||
@@ -739,7 +883,10 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
}
|
||||
if (newRotation != mRotation) {
|
||||
mRotation = newRotation;
|
||||
|
||||
if (mScreenDecorHwcLayer != null) {
|
||||
mScreenDecorHwcLayer.pendingRotationChange = false;
|
||||
mScreenDecorHwcLayer.updateRotation(mRotation);
|
||||
}
|
||||
if (mOverlays != null) {
|
||||
updateLayoutParams();
|
||||
final DisplayCutout cutout = getCutout();
|
||||
@@ -956,7 +1103,8 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
|
||||
private boolean shouldShowRoundedCorner(@BoundsPosition int pos,
|
||||
@Nullable DisplayCutout cutout) {
|
||||
return hasRoundedCorners() && isDefaultShownOverlayPos(pos, cutout);
|
||||
return hasRoundedCorners() && isDefaultShownOverlayPos(pos, cutout)
|
||||
&& mHwcScreenDecorationSupport == null;
|
||||
}
|
||||
|
||||
private boolean shouldShowPrivacyDot(@BoundsPosition int pos, @Nullable DisplayCutout cutout) {
|
||||
@@ -966,7 +1114,8 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
private boolean shouldShowCutout(@BoundsPosition int pos, @Nullable DisplayCutout cutout) {
|
||||
final Rect[] bounds = cutout == null ? null : cutout.getBoundingRectsAll();
|
||||
final int rotatedPos = getBoundPositionFromRotation(pos, mRotation);
|
||||
return (bounds != null && !bounds[rotatedPos].isEmpty());
|
||||
return (bounds != null && !bounds[rotatedPos].isEmpty()
|
||||
&& mHwcScreenDecorationSupport == null);
|
||||
}
|
||||
|
||||
private boolean shouldDrawCutout() {
|
||||
@@ -1027,14 +1176,22 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
final Drawable bottom = mRoundedCornerDrawableBottom != null
|
||||
? mRoundedCornerDrawableBottom : mRoundedCornerDrawable;
|
||||
|
||||
if (mScreenDecorHwcLayer != null) {
|
||||
mScreenDecorHwcLayer.updateRoundedCornerDrawable(top, bottom);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mOverlays == null) {
|
||||
return;
|
||||
}
|
||||
final ColorStateList colorStateList = ColorStateList.valueOf(mTintColor);
|
||||
for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
|
||||
if (mOverlays[i] == null) {
|
||||
continue;
|
||||
}
|
||||
final ViewGroup overlayView = mOverlays[i].getRootView();
|
||||
((ImageView) overlayView.findViewById(R.id.left)).setImageTintList(colorStateList);
|
||||
((ImageView) overlayView.findViewById(R.id.right)).setImageTintList(colorStateList);
|
||||
((ImageView) overlayView.findViewById(R.id.left)).setImageDrawable(
|
||||
isTopRoundedCorner(i, R.id.left) ? top : bottom);
|
||||
((ImageView) overlayView.findViewById(R.id.right)).setImageDrawable(
|
||||
@@ -1065,9 +1222,6 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
Point sizeDefault,
|
||||
Point sizeTop,
|
||||
Point sizeBottom) {
|
||||
if (mOverlays == null) {
|
||||
return;
|
||||
}
|
||||
if (sizeTop.x == 0) {
|
||||
sizeTop = sizeDefault;
|
||||
}
|
||||
@@ -1075,6 +1229,14 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
sizeBottom = sizeDefault;
|
||||
}
|
||||
|
||||
if (mScreenDecorHwcLayer != null) {
|
||||
mScreenDecorHwcLayer.updateRoundedCornerSize(sizeTop.x, sizeBottom.x);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mOverlays == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
|
||||
if (mOverlays[i] == null) {
|
||||
continue;
|
||||
@@ -1095,40 +1257,21 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
view.setLayoutParams(params);
|
||||
}
|
||||
|
||||
public static class DisplayCutoutView extends View implements DisplayManager.DisplayListener,
|
||||
RegionInterceptableView {
|
||||
|
||||
private static final float HIDDEN_CAMERA_PROTECTION_SCALE = 0.5f;
|
||||
|
||||
private Display.Mode mDisplayMode = null;
|
||||
private final DisplayInfo mInfo = new DisplayInfo();
|
||||
private final Paint mPaint = new Paint();
|
||||
public static class DisplayCutoutView extends DisplayCutoutBaseView {
|
||||
private final List<Rect> mBounds = new ArrayList();
|
||||
private final Rect mBoundingRect = new Rect();
|
||||
private final Path mBoundingPath = new Path();
|
||||
// Don't initialize these yet because they may never exist
|
||||
private RectF mProtectionRect;
|
||||
private RectF mProtectionRectOrig;
|
||||
private Path mProtectionPath;
|
||||
private Path mProtectionPathOrig;
|
||||
private Rect mTotalBounds = new Rect();
|
||||
// Whether or not to show the cutout protection path
|
||||
private boolean mShowProtection = false;
|
||||
|
||||
private final int[] mLocation = new int[2];
|
||||
private final ScreenDecorations mDecorations;
|
||||
private int mColor = Color.BLACK;
|
||||
private int mRotation;
|
||||
private int mInitialPosition;
|
||||
private int mPosition;
|
||||
private float mCameraProtectionProgress = HIDDEN_CAMERA_PROTECTION_SCALE;
|
||||
private ValueAnimator mCameraProtectionAnimator;
|
||||
|
||||
public DisplayCutoutView(Context context, @BoundsPosition int pos,
|
||||
ScreenDecorations decorations) {
|
||||
public DisplayCutoutView(Context context, @BoundsPosition int pos) {
|
||||
super(context);
|
||||
mInitialPosition = pos;
|
||||
mDecorations = decorations;
|
||||
paint.setColor(mColor);
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
setId(R.id.display_cutout);
|
||||
if (DEBUG) {
|
||||
getViewTreeObserver().addOnDrawListener(() -> Log.i(TAG,
|
||||
@@ -1138,145 +1281,31 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
|
||||
public void setColor(int color) {
|
||||
mColor = color;
|
||||
paint.setColor(mColor);
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
mContext.getSystemService(DisplayManager.class).registerDisplayListener(this,
|
||||
getHandler());
|
||||
update();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
getLocationOnScreen(mLocation);
|
||||
canvas.translate(-mLocation[0], -mLocation[1]);
|
||||
|
||||
if (!mBoundingPath.isEmpty()) {
|
||||
mPaint.setColor(mColor);
|
||||
mPaint.setStyle(Paint.Style.FILL);
|
||||
mPaint.setAntiAlias(true);
|
||||
canvas.drawPath(mBoundingPath, mPaint);
|
||||
}
|
||||
if (mCameraProtectionProgress > HIDDEN_CAMERA_PROTECTION_SCALE
|
||||
&& !mProtectionRect.isEmpty()) {
|
||||
canvas.scale(mCameraProtectionProgress, mCameraProtectionProgress,
|
||||
mProtectionRect.centerX(), mProtectionRect.centerY());
|
||||
canvas.drawPath(mProtectionPath, mPaint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisplayAdded(int displayId) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisplayRemoved(int displayId) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisplayChanged(int displayId) {
|
||||
Display.Mode oldMode = mDisplayMode;
|
||||
mDisplayMode = getDisplay().getMode();
|
||||
|
||||
// Display mode hasn't meaningfully changed, we can ignore it
|
||||
if (!modeChanged(oldMode, mDisplayMode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (displayId == getDisplay().getDisplayId()) {
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean modeChanged(Display.Mode oldMode, Display.Mode newMode) {
|
||||
if (oldMode == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean changed = false;
|
||||
changed |= oldMode.getPhysicalHeight() != newMode.getPhysicalHeight();
|
||||
changed |= oldMode.getPhysicalWidth() != newMode.getPhysicalWidth();
|
||||
// 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 changed;
|
||||
}
|
||||
|
||||
public void setRotation(int rotation) {
|
||||
public void updateRotation(int rotation) {
|
||||
mRotation = rotation;
|
||||
update();
|
||||
updateCutout();
|
||||
}
|
||||
|
||||
void setProtection(Path protectionPath, Rect pathBounds) {
|
||||
if (mProtectionPathOrig == null) {
|
||||
mProtectionPathOrig = new Path();
|
||||
mProtectionPath = new Path();
|
||||
}
|
||||
mProtectionPathOrig.set(protectionPath);
|
||||
if (mProtectionRectOrig == null) {
|
||||
mProtectionRectOrig = new RectF();
|
||||
mProtectionRect = new RectF();
|
||||
}
|
||||
mProtectionRectOrig.set(pathBounds);
|
||||
}
|
||||
|
||||
void setShowProtection(boolean shouldShow) {
|
||||
if (mShowProtection == shouldShow) {
|
||||
return;
|
||||
}
|
||||
|
||||
mShowProtection = shouldShow;
|
||||
updateBoundingPath();
|
||||
// Delay the relayout until the end of the animation when hiding the cutout,
|
||||
// otherwise we'd clip it.
|
||||
if (mShowProtection) {
|
||||
requestLayout();
|
||||
}
|
||||
if (mCameraProtectionAnimator != null) {
|
||||
mCameraProtectionAnimator.cancel();
|
||||
}
|
||||
mCameraProtectionAnimator = ValueAnimator.ofFloat(mCameraProtectionProgress,
|
||||
mShowProtection ? 1.0f : HIDDEN_CAMERA_PROTECTION_SCALE).setDuration(750);
|
||||
mCameraProtectionAnimator.setInterpolator(Interpolators.DECELERATE_QUINT);
|
||||
mCameraProtectionAnimator.addUpdateListener(animation -> {
|
||||
mCameraProtectionProgress = (float) animation.getAnimatedValue();
|
||||
invalidate();
|
||||
});
|
||||
mCameraProtectionAnimator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
mCameraProtectionAnimator = null;
|
||||
if (!mShowProtection) {
|
||||
requestLayout();
|
||||
}
|
||||
}
|
||||
});
|
||||
mCameraProtectionAnimator.start();
|
||||
}
|
||||
|
||||
private void update() {
|
||||
if (!isAttachedToWindow() || mDecorations.mPendingRotationChange) {
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
|
||||
@Override
|
||||
public void updateCutout() {
|
||||
if (!isAttachedToWindow() || pendingRotationChange) {
|
||||
return;
|
||||
}
|
||||
mPosition = getBoundPositionFromRotation(mInitialPosition, mRotation);
|
||||
requestLayout();
|
||||
getDisplay().getDisplayInfo(mInfo);
|
||||
getDisplay().getDisplayInfo(displayInfo);
|
||||
mBounds.clear();
|
||||
mBoundingRect.setEmpty();
|
||||
mBoundingPath.reset();
|
||||
cutoutPath.reset();
|
||||
int newVisible;
|
||||
if (shouldDrawCutout(getContext()) && hasCutout()) {
|
||||
mBounds.addAll(mInfo.displayCutout.getBoundingRects());
|
||||
mBounds.addAll(displayInfo.displayCutout.getBoundingRects());
|
||||
localBounds(mBoundingRect);
|
||||
updateGravity();
|
||||
updateBoundingPath();
|
||||
@@ -1291,10 +1320,11 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
}
|
||||
|
||||
private void updateBoundingPath() {
|
||||
int lw = mInfo.logicalWidth;
|
||||
int lh = mInfo.logicalHeight;
|
||||
int lw = displayInfo.logicalWidth;
|
||||
int lh = displayInfo.logicalHeight;
|
||||
|
||||
boolean flipped = mInfo.rotation == ROTATION_90 || mInfo.rotation == ROTATION_270;
|
||||
boolean flipped = displayInfo.rotation == ROTATION_90
|
||||
|| displayInfo.rotation == ROTATION_270;
|
||||
|
||||
int dw = flipped ? lh : lw;
|
||||
int dh = flipped ? lw : lh;
|
||||
@@ -1302,49 +1332,20 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
Path path = DisplayCutout.pathFromResources(
|
||||
getResources(), getDisplay().getUniqueId(), dw, dh);
|
||||
if (path != null) {
|
||||
mBoundingPath.set(path);
|
||||
cutoutPath.set(path);
|
||||
} else {
|
||||
mBoundingPath.reset();
|
||||
cutoutPath.reset();
|
||||
}
|
||||
Matrix m = new Matrix();
|
||||
transformPhysicalToLogicalCoordinates(mInfo.rotation, dw, dh, m);
|
||||
mBoundingPath.transform(m);
|
||||
if (mProtectionPathOrig != null) {
|
||||
// Reset the protection path so we don't aggregate rotations
|
||||
mProtectionPath.set(mProtectionPathOrig);
|
||||
mProtectionPath.transform(m);
|
||||
m.mapRect(mProtectionRect, mProtectionRectOrig);
|
||||
}
|
||||
}
|
||||
|
||||
private static void transformPhysicalToLogicalCoordinates(@Surface.Rotation int rotation,
|
||||
@Dimension int physicalWidth, @Dimension int physicalHeight, Matrix out) {
|
||||
switch (rotation) {
|
||||
case ROTATION_0:
|
||||
out.reset();
|
||||
break;
|
||||
case ROTATION_90:
|
||||
out.setRotate(270);
|
||||
out.postTranslate(0, physicalWidth);
|
||||
break;
|
||||
case ROTATION_180:
|
||||
out.setRotate(180);
|
||||
out.postTranslate(physicalWidth, physicalHeight);
|
||||
break;
|
||||
case ROTATION_270:
|
||||
out.setRotate(90);
|
||||
out.postTranslate(physicalHeight, 0);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown rotation: " + rotation);
|
||||
}
|
||||
transformPhysicalToLogicalCoordinates(displayInfo.rotation, dw, dh, m);
|
||||
cutoutPath.transform(m);
|
||||
}
|
||||
|
||||
private void updateGravity() {
|
||||
LayoutParams lp = getLayoutParams();
|
||||
if (lp instanceof FrameLayout.LayoutParams) {
|
||||
FrameLayout.LayoutParams flp = (FrameLayout.LayoutParams) lp;
|
||||
int newGravity = getGravity(mInfo.displayCutout);
|
||||
int newGravity = getGravity(displayInfo.displayCutout);
|
||||
if (flp.gravity != newGravity) {
|
||||
flp.gravity = newGravity;
|
||||
setLayoutParams(flp);
|
||||
@@ -1353,7 +1354,7 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
}
|
||||
|
||||
private boolean hasCutout() {
|
||||
final DisplayCutout displayCutout = mInfo.displayCutout;
|
||||
final DisplayCutout displayCutout = displayInfo.displayCutout;
|
||||
if (displayCutout == null) {
|
||||
return false;
|
||||
}
|
||||
@@ -1377,11 +1378,11 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
return;
|
||||
}
|
||||
|
||||
if (mShowProtection) {
|
||||
if (showProtection) {
|
||||
// Make sure that our measured height encompases the protection
|
||||
mTotalBounds.union(mBoundingRect);
|
||||
mTotalBounds.union((int) mProtectionRect.left, (int) mProtectionRect.top,
|
||||
(int) mProtectionRect.right, (int) mProtectionRect.bottom);
|
||||
mTotalBounds.union((int) protectionRect.left, (int) protectionRect.top,
|
||||
(int) protectionRect.right, (int) protectionRect.bottom);
|
||||
setMeasuredDimension(
|
||||
resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0),
|
||||
resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0));
|
||||
@@ -1413,7 +1414,7 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
}
|
||||
|
||||
private void localBounds(Rect out) {
|
||||
DisplayCutout displayCutout = mInfo.displayCutout;
|
||||
DisplayCutout displayCutout = displayInfo.displayCutout;
|
||||
boundsFromDirection(displayCutout, getGravity(displayCutout), out);
|
||||
}
|
||||
|
||||
@@ -1437,32 +1438,6 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
}
|
||||
return Gravity.NO_GRAVITY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldInterceptTouch() {
|
||||
return mInfo.displayCutout != null && getVisibility() == VISIBLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Region getInterceptRegion() {
|
||||
if (mInfo.displayCutout == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
View rootView = getRootView();
|
||||
Region cutoutBounds = rectsToRegion(
|
||||
mInfo.displayCutout.getBoundingRects());
|
||||
|
||||
// Transform to window's coordinate space
|
||||
rootView.getLocationOnScreen(mLocation);
|
||||
cutoutBounds.translate(-mLocation[0], -mLocation[1]);
|
||||
|
||||
// Intersect with window's frame
|
||||
cutoutBounds.op(rootView.getLeft(), rootView.getTop(), rootView.getRight(),
|
||||
rootView.getBottom(), Region.Op.INTERSECT);
|
||||
|
||||
return cutoutBounds;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1473,6 +1448,8 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
|
||||
private final View mView;
|
||||
private final int mTargetRotation;
|
||||
// Pass -1 for ScreenDecorHwcLayer since it's a fullscreen window and has no specific
|
||||
// position.
|
||||
private final int mPosition;
|
||||
|
||||
private RestartingPreDrawListener(View view, @BoundsPosition int position,
|
||||
@@ -1488,7 +1465,9 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
|
||||
if (mTargetRotation == mRotation) {
|
||||
if (DEBUG) {
|
||||
Log.i(TAG, getWindowTitleByPos(mPosition) + " already in target rot "
|
||||
final String title = mPosition < 0 ? "ScreenDecorHwcLayer"
|
||||
: getWindowTitleByPos(mPosition);
|
||||
Log.i(TAG, title + " already in target rot "
|
||||
+ mTargetRotation + ", allow draw without restarting it");
|
||||
}
|
||||
return true;
|
||||
@@ -1499,7 +1478,9 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
|
||||
// take effect.
|
||||
updateOrientation();
|
||||
if (DEBUG) {
|
||||
Log.i(TAG, getWindowTitleByPos(mPosition)
|
||||
final String title = mPosition < 0 ? "ScreenDecorHwcLayer"
|
||||
: getWindowTitleByPos(mPosition);
|
||||
Log.i(TAG, title
|
||||
+ " restarting listener fired, restarting draw for rot " + mRotation);
|
||||
}
|
||||
mView.invalidate();
|
||||
|
||||
@@ -92,6 +92,8 @@ class PrivacyDotViewController @Inject constructor(
|
||||
private val views: Sequence<View>
|
||||
get() = if (!this::tl.isInitialized) sequenceOf() else sequenceOf(tl, tr, br, bl)
|
||||
|
||||
private var showingListener: ShowingListener? = null
|
||||
|
||||
init {
|
||||
contentInsetsProvider.addCallback(object : StatusBarContentInsetsChangedListener {
|
||||
override fun onStatusBarContentInsetsChanged() {
|
||||
@@ -132,6 +134,10 @@ class PrivacyDotViewController @Inject constructor(
|
||||
uiExecutor = e
|
||||
}
|
||||
|
||||
fun setShowingListener(l: ShowingListener) {
|
||||
showingListener = l
|
||||
}
|
||||
|
||||
fun setQsExpanded(expanded: Boolean) {
|
||||
dlog("setQsExpanded $expanded")
|
||||
synchronized(lock) {
|
||||
@@ -176,15 +182,20 @@ class PrivacyDotViewController @Inject constructor(
|
||||
.setDuration(DURATION)
|
||||
.setInterpolator(Interpolators.ALPHA_OUT)
|
||||
.alpha(0f)
|
||||
.withEndAction { dot.visibility = View.INVISIBLE }
|
||||
.withEndAction {
|
||||
dot.visibility = View.INVISIBLE
|
||||
showingListener?.onPrivacyDotHidden(dot)
|
||||
}
|
||||
.start()
|
||||
} else {
|
||||
dot.visibility = View.INVISIBLE
|
||||
showingListener?.onPrivacyDotHidden(dot)
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private fun showDotView(dot: View, animate: Boolean) {
|
||||
showingListener?.onPrivacyDotShown(dot)
|
||||
dot.clearAnimation()
|
||||
if (animate) {
|
||||
dot.visibility = View.VISIBLE
|
||||
@@ -320,6 +331,7 @@ class PrivacyDotViewController @Inject constructor(
|
||||
@UiThread
|
||||
private fun updateDesignatedCorner(newCorner: View?, shouldShowDot: Boolean) {
|
||||
if (shouldShowDot) {
|
||||
showingListener?.onPrivacyDotShown(newCorner)
|
||||
newCorner?.apply {
|
||||
clearAnimation()
|
||||
visibility = View.VISIBLE
|
||||
@@ -336,6 +348,11 @@ class PrivacyDotViewController @Inject constructor(
|
||||
private fun setCornerVisibilities(vis: Int) {
|
||||
views.forEach { corner ->
|
||||
corner.visibility = vis
|
||||
if (vis == View.VISIBLE) {
|
||||
showingListener?.onPrivacyDotShown(corner)
|
||||
} else {
|
||||
showingListener?.onPrivacyDotHidden(corner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,6 +572,11 @@ class PrivacyDotViewController @Inject constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public interface ShowingListener {
|
||||
fun onPrivacyDotShown(v: View?)
|
||||
fun onPrivacyDotHidden(v: View?)
|
||||
}
|
||||
}
|
||||
|
||||
private fun dlog(s: String) {
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* 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.Canvas
|
||||
import android.graphics.Insets
|
||||
import android.graphics.Path
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.graphics.Region
|
||||
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.dx.mockito.inline.extended.ExtendedMockito.never
|
||||
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.spy
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.MockitoAnnotations
|
||||
import org.mockito.Mockito.`when` as whenever
|
||||
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
@SmallTest
|
||||
class DisplayCutoutBaseViewTest : SysuiTestCase() {
|
||||
|
||||
@Mock private lateinit var mockCanvas: Canvas
|
||||
@Mock private lateinit var mockRootView: View
|
||||
@Mock private lateinit var mockDisplay: Display
|
||||
|
||||
private lateinit var cutoutBaseView: DisplayCutoutBaseView
|
||||
private val cutout: DisplayCutout = DisplayCutout.Builder()
|
||||
.setSafeInsets(Insets.of(0, 2, 0, 0))
|
||||
.setBoundingRectTop(Rect(1, 0, 2, 2))
|
||||
.build()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBoundingRectsToRegion() {
|
||||
setupDisplayCutoutBaseView(true /* fillCutout */, true /* hasCutout */)
|
||||
val rect = Rect(1, 2, 3, 4)
|
||||
assertThat(cutoutBaseView.rectsToRegion(listOf(rect)).bounds).isEqualTo(rect)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDrawCutout_fillCutout() {
|
||||
setupDisplayCutoutBaseView(true /* fillCutout */, true /* hasCutout */)
|
||||
cutoutBaseView.onDraw(mockCanvas)
|
||||
|
||||
verify(cutoutBaseView).drawCutouts(mockCanvas)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDrawCutout_notFillCutout() {
|
||||
setupDisplayCutoutBaseView(false /* fillCutout */, true /* hasCutout */)
|
||||
cutoutBaseView.onDraw(mockCanvas)
|
||||
|
||||
verify(cutoutBaseView, never()).drawCutouts(mockCanvas)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testShouldInterceptTouch_hasCutout() {
|
||||
setupDisplayCutoutBaseView(true /* fillCutout */, true /* hasCutout */)
|
||||
cutoutBaseView.updateCutout()
|
||||
|
||||
assertThat(cutoutBaseView.shouldInterceptTouch()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testShouldInterceptTouch_noCutout() {
|
||||
setupDisplayCutoutBaseView(true /* fillCutout */, false /* hasCutout */)
|
||||
cutoutBaseView.updateCutout()
|
||||
|
||||
assertThat(cutoutBaseView.shouldInterceptTouch()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetInterceptRegion_hasCutout() {
|
||||
setupDisplayCutoutBaseView(true /* fillCutout */, true /* hasCutout */)
|
||||
whenever(mockRootView.left).thenReturn(0)
|
||||
whenever(mockRootView.top).thenReturn(0)
|
||||
whenever(mockRootView.right).thenReturn(100)
|
||||
whenever(mockRootView.bottom).thenReturn(200)
|
||||
|
||||
val expect = Region()
|
||||
expect.op(cutout.boundingRectTop, Region.Op.UNION)
|
||||
expect.op(0, 0, 100, 200, Region.Op.INTERSECT)
|
||||
|
||||
cutoutBaseView.updateCutout()
|
||||
|
||||
assertThat(cutoutBaseView.interceptRegion).isEqualTo(expect)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetInterceptRegion_noCutout() {
|
||||
setupDisplayCutoutBaseView(true /* fillCutout */, false /* hasCutout */)
|
||||
cutoutBaseView.updateCutout()
|
||||
|
||||
assertThat(cutoutBaseView.interceptRegion).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCutoutProtection() {
|
||||
setupDisplayCutoutBaseView(true /* fillCutout */, false /* hasCutout */)
|
||||
val bounds = Rect(0, 0, 10, 10)
|
||||
val path = Path()
|
||||
val pathBounds = RectF(bounds)
|
||||
path.addRect(pathBounds, Path.Direction.CCW)
|
||||
|
||||
context.mainExecutor.execute {
|
||||
cutoutBaseView.setProtection(path, bounds)
|
||||
cutoutBaseView.enableShowProtection(true)
|
||||
}
|
||||
waitForIdleSync()
|
||||
|
||||
assertThat(cutoutBaseView.protectionPath.isRect(pathBounds)).isTrue()
|
||||
assertThat(cutoutBaseView.protectionRect).isEqualTo(pathBounds)
|
||||
}
|
||||
|
||||
private fun setupDisplayCutoutBaseView(fillCutout: Boolean, hasCutout: Boolean) {
|
||||
mContext.orCreateTestableResources.addOverride(
|
||||
R.array.config_displayUniqueIdArray, arrayOf<String>())
|
||||
mContext.orCreateTestableResources.addOverride(
|
||||
R.bool.config_fillMainBuiltInDisplayCutout, fillCutout)
|
||||
|
||||
cutoutBaseView = spy(DisplayCutoutBaseView(mContext))
|
||||
whenever(cutoutBaseView.display).thenReturn(mockDisplay)
|
||||
whenever(cutoutBaseView.rootView).thenReturn(mockRootView)
|
||||
whenever(mockDisplay.getDisplayInfo(eq(cutoutBaseView.displayInfo))
|
||||
).then {
|
||||
val info = it.getArgument<DisplayInfo>(0)
|
||||
info.displayCutout = if (hasCutout) cutout else null
|
||||
return@then true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@
|
||||
|
||||
package com.android.systemui;
|
||||
|
||||
import static android.view.Display.DEFAULT_DISPLAY;
|
||||
import static android.view.DisplayCutout.BOUNDS_POSITION_BOTTOM;
|
||||
import static android.view.DisplayCutout.BOUNDS_POSITION_LEFT;
|
||||
import static android.view.DisplayCutout.BOUNDS_POSITION_LENGTH;
|
||||
@@ -22,7 +21,7 @@ import static android.view.DisplayCutout.BOUNDS_POSITION_RIGHT;
|
||||
import static android.view.DisplayCutout.BOUNDS_POSITION_TOP;
|
||||
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
|
||||
|
||||
import static com.android.systemui.ScreenDecorations.rectsToRegion;
|
||||
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
@@ -32,7 +31,6 @@ import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isA;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
@@ -49,10 +47,12 @@ import android.annotation.IdRes;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Insets;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.VectorDrawable;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.hardware.graphics.common.DisplayDecorationSupport;
|
||||
import android.os.Handler;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.testing.TestableLooper;
|
||||
@@ -89,7 +89,6 @@ import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
||||
@RunWithLooper
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
@@ -106,6 +105,8 @@ public class ScreenDecorationsTest extends SysuiTestCase {
|
||||
private FakeThreadFactory mThreadFactory;
|
||||
private ArrayList<DecorProvider> mDecorProviders;
|
||||
@Mock
|
||||
private Display mDisplay;
|
||||
@Mock
|
||||
private TunerService mTunerService;
|
||||
@Mock
|
||||
private BroadcastDispatcher mBroadcastDispatcher;
|
||||
@@ -140,14 +141,15 @@ public class ScreenDecorationsTest extends SysuiTestCase {
|
||||
.getMaximumWindowMetrics();
|
||||
when(mWindowManager.getMaximumWindowMetrics()).thenReturn(metrics);
|
||||
mContext.addMockSystemService(WindowManager.class, mWindowManager);
|
||||
|
||||
mDisplayManager = mock(DisplayManager.class);
|
||||
Display display = mContext.getSystemService(DisplayManager.class)
|
||||
.getDisplay(DEFAULT_DISPLAY);
|
||||
when(mDisplayManager.getDisplay(anyInt())).thenReturn(display);
|
||||
mContext.addMockSystemService(DisplayManager.class, mDisplayManager);
|
||||
when(mMockTypedArray.length()).thenReturn(0);
|
||||
|
||||
spyOn(mContext);
|
||||
when(mContext.getDisplay()).thenReturn(mDisplay);
|
||||
// Not support hwc layer by default
|
||||
doReturn(null).when(mDisplay).getDisplayDecorationSupport();
|
||||
|
||||
when(mMockTypedArray.length()).thenReturn(0);
|
||||
mPrivacyDotTopLeftDecorProvider = spy(new PrivacyDotCornerDecorProviderImpl(
|
||||
R.id.privacy_dot_top_left_container,
|
||||
DisplayCutout.BOUNDS_POSITION_TOP,
|
||||
@@ -974,12 +976,6 @@ public class ScreenDecorationsTest extends SysuiTestCase {
|
||||
assertEquals(new Point(20, 20), mScreenDecorations.mRoundedDefaultBottom);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBoundingRectsToRegion() throws Exception {
|
||||
Rect rect = new Rect(1, 2, 3, 4);
|
||||
assertThat(rectsToRegion(Collections.singletonList(rect)).getBounds(), is(rect));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegistration_From_NoOverlay_To_HasOverlays() {
|
||||
doReturn(false).when(mScreenDecorations).hasOverlays();
|
||||
@@ -1029,6 +1025,114 @@ public class ScreenDecorationsTest extends SysuiTestCase {
|
||||
assertThat(mScreenDecorations.mIsRegistered, is(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSupportHwcLayer_SwitchFrom_NotSupport() {
|
||||
setupResources(0 /* radius */, 10 /* radiusTop */, 20 /* radiusBottom */,
|
||||
0 /* roundedPadding */, false /* multipleRadius */,
|
||||
true /* fillCutout */, false /* privacyDot */);
|
||||
|
||||
// top cutout
|
||||
final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null};
|
||||
doReturn(getDisplayCutoutForRotation(Insets.of(0, 1, 0, 0), bounds))
|
||||
.when(mScreenDecorations).getCutout();
|
||||
|
||||
mScreenDecorations.start();
|
||||
// should only inflate mOverlays when the hwc doesn't support screen decoration
|
||||
assertNull(mScreenDecorations.mScreenDecorHwcWindow);
|
||||
assertNotNull(mScreenDecorations.mOverlays);
|
||||
assertNotNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]);
|
||||
assertNotNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]);
|
||||
|
||||
final DisplayDecorationSupport decorationSupport = new DisplayDecorationSupport();
|
||||
decorationSupport.format = PixelFormat.R_8;
|
||||
doReturn(decorationSupport).when(mDisplay).getDisplayDecorationSupport();
|
||||
// Trigger the support hwc screen decoration change by changing the display unique id
|
||||
mScreenDecorations.mDisplayUniqueId = "test";
|
||||
mScreenDecorations.mDisplayListener.onDisplayChanged(1);
|
||||
|
||||
// should only inflate hwc layer when the hwc supports screen decoration
|
||||
assertNotNull(mScreenDecorations.mScreenDecorHwcWindow);
|
||||
assertNull(mScreenDecorations.mOverlays);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotSupportHwcLayer_SwitchFrom_Support() {
|
||||
setupResources(0 /* radius */, 10 /* radiusTop */, 20 /* radiusBottom */,
|
||||
0 /* roundedPadding */, false /* multipleRadius */,
|
||||
true /* fillCutout */, false /* privacyDot */);
|
||||
final DisplayDecorationSupport decorationSupport = new DisplayDecorationSupport();
|
||||
decorationSupport.format = PixelFormat.R_8;
|
||||
doReturn(decorationSupport).when(mDisplay).getDisplayDecorationSupport();
|
||||
|
||||
// top cutout
|
||||
final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null};
|
||||
doReturn(getDisplayCutoutForRotation(Insets.of(0, 1, 0, 0), bounds))
|
||||
.when(mScreenDecorations).getCutout();
|
||||
|
||||
mScreenDecorations.start();
|
||||
// should only inflate hwc layer when the hwc supports screen decoration
|
||||
assertNotNull(mScreenDecorations.mScreenDecorHwcWindow);
|
||||
assertNull(mScreenDecorations.mOverlays);
|
||||
|
||||
doReturn(null).when(mDisplay).getDisplayDecorationSupport();
|
||||
// Trigger the support hwc screen decoration change by changing the display unique id
|
||||
mScreenDecorations.mDisplayUniqueId = "test";
|
||||
mScreenDecorations.mDisplayListener.onDisplayChanged(1);
|
||||
|
||||
// should only inflate mOverlays when the hwc doesn't support screen decoration
|
||||
assertNull(mScreenDecorations.mScreenDecorHwcWindow);
|
||||
assertNotNull(mScreenDecorations.mOverlays);
|
||||
assertNotNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]);
|
||||
assertNotNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHwcLayer_noPrivacyDot() {
|
||||
setupResources(0 /* radius */, 10 /* radiusTop */, 20 /* radiusBottom */,
|
||||
0 /* roundedPadding */, false /* multipleRadius */,
|
||||
true /* fillCutout */, false /* privacyDot */);
|
||||
final DisplayDecorationSupport decorationSupport = new DisplayDecorationSupport();
|
||||
decorationSupport.format = PixelFormat.R_8;
|
||||
doReturn(decorationSupport).when(mDisplay).getDisplayDecorationSupport();
|
||||
|
||||
// top cutout
|
||||
final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null};
|
||||
doReturn(getDisplayCutoutForRotation(Insets.of(0, 1, 0, 0), bounds))
|
||||
.when(mScreenDecorations).getCutout();
|
||||
|
||||
mScreenDecorations.start();
|
||||
|
||||
// Should only inflate hwc layer.
|
||||
assertNotNull(mScreenDecorations.mScreenDecorHwcWindow);
|
||||
assertNull(mScreenDecorations.mOverlays);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHwcLayer_PrivacyDot() {
|
||||
setupResources(0 /* radius */, 10 /* radiusTop */, 20 /* radiusBottom */,
|
||||
0 /* roundedPadding */, false /* multipleRadius */,
|
||||
true /* fillCutout */, true /* privacyDot */);
|
||||
final DisplayDecorationSupport decorationSupport = new DisplayDecorationSupport();
|
||||
decorationSupport.format = PixelFormat.R_8;
|
||||
doReturn(decorationSupport).when(mDisplay).getDisplayDecorationSupport();
|
||||
|
||||
// top cutout
|
||||
final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null};
|
||||
doReturn(getDisplayCutoutForRotation(Insets.of(0, 1, 0, 0), bounds))
|
||||
.when(mScreenDecorations).getCutout();
|
||||
|
||||
mScreenDecorations.start();
|
||||
|
||||
assertNotNull(mScreenDecorations.mScreenDecorHwcWindow);
|
||||
// mOverlays are inflated but the visibility should be GONE.
|
||||
assertNotNull(mScreenDecorations.mOverlays);
|
||||
final View topOverlay = mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP].getRootView();
|
||||
final View botOverlay = mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM].getRootView();
|
||||
assertEquals(topOverlay.getVisibility(), View.INVISIBLE);
|
||||
assertEquals(botOverlay.getVisibility(), View.INVISIBLE);
|
||||
|
||||
}
|
||||
|
||||
private void setupResources(int radius, int radiusTop, int radiusBottom, int roundedPadding,
|
||||
boolean multipleRadius, boolean fillCutout, boolean privacyDot) {
|
||||
mContext.getOrCreateTestableResources().addOverride(
|
||||
|
||||
Reference in New Issue
Block a user