diff --git a/packages/SystemUI/res/layout/rounded_corners_bottom.xml b/packages/SystemUI/res/layout/rounded_corners_bottom.xml index b2857abc19d2c..bb6d4bddf25ad 100644 --- a/packages/SystemUI/res/layout/rounded_corners_bottom.xml +++ b/packages/SystemUI/res/layout/rounded_corners_bottom.xml @@ -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"/> diff --git a/packages/SystemUI/res/layout/rounded_corners_top.xml b/packages/SystemUI/res/layout/rounded_corners_top.xml index 9937c215e71cd..46648c88d921e 100644 --- a/packages/SystemUI/res/layout/rounded_corners_top.xml +++ b/packages/SystemUI/res/layout/rounded_corners_top.xml @@ -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"/> diff --git a/packages/SystemUI/res/layout/screen_decor_hwc_layer.xml b/packages/SystemUI/res/layout/screen_decor_hwc_layer.xml new file mode 100644 index 0000000000000..1c17e7512491d --- /dev/null +++ b/packages/SystemUI/res/layout/screen_decor_hwc_layer.xml @@ -0,0 +1,21 @@ + + + diff --git a/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt b/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt new file mode 100644 index 0000000000000..6bec8aacd55fe --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt @@ -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?): Region { + val result = Region.obtain() + if (rects != null) { + for (r in rects) { + if (r != null && !r.isEmpty) { + result.op(r, Region.Op.UNION) + } + } + } + return result + } + + open fun enableShowProtection(show: Boolean) { + if (showProtection == show) { + return + } + showProtection = show + updateProtectionBoundingPath() + // Delay the relayout until the end of the animation when hiding the cutout, + // otherwise we'd clip it. + if (showProtection) { + requestLayout() + } + cameraProtectionAnimator?.cancel() + cameraProtectionAnimator = ValueAnimator.ofFloat(cameraProtectionProgress, + if (showProtection) 1.0f else HIDDEN_CAMERA_PROTECTION_SCALE).setDuration(750) + cameraProtectionAnimator?.interpolator = Interpolators.DECELERATE_QUINT + cameraProtectionAnimator?.addUpdateListener(ValueAnimator.AnimatorUpdateListener { + animation: ValueAnimator -> + cameraProtectionProgress = animation.animatedValue as Float + invalidate() + }) + cameraProtectionAnimator?.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + cameraProtectionAnimator = null + if (!showProtection) { + requestLayout() + } + } + }) + cameraProtectionAnimator?.start() + } + + open fun setProtection(path: Path, pathBounds: Rect) { + protectionPathOrig.reset() + protectionPathOrig.set(path) + protectionPath.reset() + protectionRectOrig.setEmpty() + protectionRectOrig.set(pathBounds) + protectionRect.setEmpty() + } + + protected open fun updateProtectionBoundingPath() { + if (pendingRotationChange) { + return + } + val lw: Int = displayInfo.logicalWidth + val lh: Int = displayInfo.logicalHeight + val flipped = (displayInfo.rotation == Surface.ROTATION_90 || + displayInfo.rotation == Surface.ROTATION_270) + val dw = if (flipped) lh else lw + val dh = if (flipped) lw else lh + val m = Matrix() + transformPhysicalToLogicalCoordinates(displayInfo.rotation, dw, dh, m) + if (!protectionPathOrig.isEmpty) { + // Reset the protection path so we don't aggregate rotations + protectionPath.set(protectionPathOrig) + protectionPath.transform(m) + m.mapRect(protectionRect, protectionRectOrig) + } + } + + private fun displayModeChanged(oldMode: Display.Mode?, newMode: Display.Mode?): Boolean { + if (oldMode == null) { + return true + } + + // We purposely ignore refresh rate and id changes here, because we don't need to + // invalidate for those, and they can trigger the refresh rate to increase + return oldMode?.physicalHeight != newMode?.physicalHeight || + oldMode?.physicalWidth != newMode?.physicalWidth + } + + companion object { + private const val HIDDEN_CAMERA_PROTECTION_SCALE = 0.5f + + @JvmStatic protected fun transformPhysicalToLogicalCoordinates( + @Surface.Rotation rotation: Int, + @Dimension physicalWidth: Int, + @Dimension physicalHeight: Int, + out: Matrix + ) { + when (rotation) { + Surface.ROTATION_0 -> out.reset() + Surface.ROTATION_90 -> { + out.setRotate(270f) + out.postTranslate(0f, physicalWidth.toFloat()) + } + Surface.ROTATION_180 -> { + out.setRotate(180f) + out.postTranslate(physicalWidth.toFloat(), physicalHeight.toFloat()) + } + Surface.ROTATION_270 -> { + out.setRotate(90f) + out.postTranslate(physicalHeight.toFloat(), 0f) + } + else -> throw IllegalArgumentException("Unknown rotation: $rotation") + } + } + } +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorHwcLayer.kt b/packages/SystemUI/src/com/android/systemui/ScreenDecorHwcLayer.kt new file mode 100644 index 0000000000000..ee1d9a3aa5de4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorHwcLayer.kt @@ -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 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java index 783415e98875f..2ec5f4f894f93 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java @@ -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 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 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 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(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt index 962c7fa6aea78..140142394c248 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt @@ -92,6 +92,8 @@ class PrivacyDotViewController @Inject constructor( private val views: Sequence 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) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/DisplayCutoutBaseViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/DisplayCutoutBaseViewTest.kt new file mode 100644 index 0000000000000..e62b4e63e3d5d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/DisplayCutoutBaseViewTest.kt @@ -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()) + 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(0) + info.displayCutout = if (hasCutout) cutout else null + return@then true + } + } +} \ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java index 72d72c8c3b5e6..70f325158624e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java @@ -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 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(