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
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