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:
shawnlin
2022-01-21 21:45:01 +08:00
parent 52855a06e9
commit eb33c9cb51
9 changed files with 1064 additions and 300 deletions

View File

@@ -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"/>

View File

@@ -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"/>

View 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" />

View File

@@ -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")
}
}
}
}

View File

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

View File

@@ -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();

View File

@@ -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) {

View File

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

View File

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