[Decor] Add DebugRoundedCornerDelegate

This CL introduces the ability to have debug rounded corners in
ScreenDecorations. It does this by creating a DebugRoundedCornerDelegate
that maintains the debug state for rounded corners, and modifying
ScreenDecorations to read from that delegate when in debug mode AND the
delegate has providers.

This means that debug mode actually has 2 stages:
1. simply turning on debug mode and providing no extra rounded corner
  information will change the device-default corners to show up in
  screenshots and display in color.
2. Secondly, providing a debug corner path spec will switch from using
   the rounded corner delegate to the new debug delegate. This switching
   is to-be-defined in a future CL, but it will involve re-solving for
   the providers and overlays.

Test: manual
Test: ScreenDecorationsTest
Bug: 285941724
Change-Id: Ia0ce4c06117a2877cc930a1c1683912a383a968c
This commit is contained in:
Evan Laird
2023-06-08 13:34:20 -04:00
parent eba0b0f0df
commit d8e648ee83
3 changed files with 327 additions and 13 deletions

View File

@@ -71,6 +71,7 @@ import com.android.systemui.biometrics.AuthController;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.decor.CutoutDecorProviderFactory;
import com.android.systemui.decor.DebugRoundedCornerDelegate;
import com.android.systemui.decor.DecorProvider;
import com.android.systemui.decor.DecorProviderFactory;
import com.android.systemui.decor.DecorProviderKt;
@@ -145,6 +146,10 @@ public class ScreenDecorations implements CoreStartable, Dumpable {
protected RoundedCornerResDelegateImpl mRoundedCornerResDelegate;
@VisibleForTesting
protected DecorProviderFactory mRoundedCornerFactory;
@VisibleForTesting
protected DebugRoundedCornerDelegate mDebugRoundedCornerDelegate =
new DebugRoundedCornerDelegate();
protected DecorProviderFactory mDebugRoundedCornerFactory;
private CutoutDecorProviderFactory mCutoutFactory;
private int mProviderRefreshToken = 0;
@VisibleForTesting
@@ -363,11 +368,17 @@ public class ScreenDecorations implements CoreStartable, Dumpable {
* it requires essentially re-init-ing this screen decorations process with the debug
* information taken into account.
*/
private void setDebug(boolean debug) {
@VisibleForTesting
protected void setDebug(boolean debug) {
if (mDebug == debug) {
return;
}
mDebug = debug;
if (!mDebug) {
mDebugRoundedCornerDelegate.removeDebugState();
}
mExecutor.execute(() -> {
// Re-trigger all of the screen decorations setup here so that the debug values
// can be picked up
@@ -383,11 +394,16 @@ public class ScreenDecorations implements CoreStartable, Dumpable {
}
@NonNull
private List<DecorProvider> getProviders(boolean hasHwLayer) {
@VisibleForTesting
protected List<DecorProvider> getProviders(boolean hasHwLayer) {
List<DecorProvider> decorProviders = new ArrayList<>(mDotFactory.getProviders());
decorProviders.addAll(mFaceScanningFactory.getProviders());
if (!hasHwLayer) {
decorProviders.addAll(mRoundedCornerFactory.getProviders());
if (mDebug && mDebugRoundedCornerFactory.getHasProviders()) {
decorProviders.addAll(mDebugRoundedCornerFactory.getProviders());
} else {
decorProviders.addAll(mRoundedCornerFactory.getProviders());
}
decorProviders.addAll(mCutoutFactory.getProviders());
}
return decorProviders;
@@ -434,6 +450,8 @@ public class ScreenDecorations implements CoreStartable, Dumpable {
mRoundedCornerResDelegate.setPhysicalPixelDisplaySizeRatio(
getPhysicalPixelDisplaySizeRatio());
mRoundedCornerFactory = new RoundedCornerDecorProviderFactory(mRoundedCornerResDelegate);
mDebugRoundedCornerFactory =
new RoundedCornerDecorProviderFactory(mDebugRoundedCornerDelegate);
mCutoutFactory = getCutoutFactory();
mHwcScreenDecorationSupport = mContext.getDisplay().getDisplayDecorationSupport();
updateHwLayerRoundedCornerDrawable();
@@ -966,6 +984,8 @@ public class ScreenDecorations implements CoreStartable, Dumpable {
mTintColor = colorsInvertedValue != 0 ? Color.WHITE : Color.BLACK;
if (mDebug) {
mTintColor = mDebugColor;
mDebugRoundedCornerDelegate.setColor(mTintColor);
//TODO(b/285941724): update the hwc layer color here too (or disable it in debug mode)
}
updateOverlayProviderViews(new Integer[] {
@@ -1038,6 +1058,7 @@ public class ScreenDecorations implements CoreStartable, Dumpable {
if (DEBUG_DISABLE_SCREEN_DECORATIONS) {
return;
}
ipw.println("mDebug:" + mDebug);
ipw.println("mIsPrivacyDotEnabled:" + isPrivacyDotEnabled());
ipw.println("shouldOptimizeOverlayVisibility:" + shouldOptimizeVisibility());
@@ -1093,6 +1114,7 @@ public class ScreenDecorations implements CoreStartable, Dumpable {
}
}
mRoundedCornerResDelegate.dump(pw, args);
mDebugRoundedCornerDelegate.dump(pw);
}
@VisibleForTesting
@@ -1115,8 +1137,9 @@ public class ScreenDecorations implements CoreStartable, Dumpable {
mRotation = newRotation;
mDisplayMode = newMod;
mDisplayCutout = newCutout;
mRoundedCornerResDelegate.setPhysicalPixelDisplaySizeRatio(
getPhysicalPixelDisplaySizeRatio());
float ratio = getPhysicalPixelDisplaySizeRatio();
mRoundedCornerResDelegate.setPhysicalPixelDisplaySizeRatio(ratio);
mDebugRoundedCornerDelegate.setPhysicalPixelDisplaySizeRatio(ratio);
if (mScreenDecorHwcLayer != null) {
mScreenDecorHwcLayer.pendingConfigChange = false;
mScreenDecorHwcLayer.updateConfiguration(mDisplayUniqueId);
@@ -1139,7 +1162,8 @@ public class ScreenDecorations implements CoreStartable, Dumpable {
}
private boolean hasRoundedCorners() {
return mRoundedCornerFactory.getHasProviders();
return mRoundedCornerFactory.getHasProviders()
|| mDebugRoundedCornerFactory.getHasProviders();
}
private boolean shouldOptimizeVisibility() {
@@ -1197,8 +1221,12 @@ public class ScreenDecorations implements CoreStartable, Dumpable {
return;
}
final Drawable topDrawable = mRoundedCornerResDelegate.getTopRoundedDrawable();
final Drawable bottomDrawable = mRoundedCornerResDelegate.getBottomRoundedDrawable();
Drawable topDrawable = mRoundedCornerResDelegate.getTopRoundedDrawable();
Drawable bottomDrawable = mRoundedCornerResDelegate.getBottomRoundedDrawable();
if (mDebug && (mDebugRoundedCornerFactory.getHasProviders())) {
topDrawable = mDebugRoundedCornerDelegate.getTopRoundedDrawable();
bottomDrawable = mDebugRoundedCornerDelegate.getBottomRoundedDrawable();
}
if (topDrawable == null || bottomDrawable == null) {
return;
@@ -1210,11 +1238,19 @@ public class ScreenDecorations implements CoreStartable, Dumpable {
if (mScreenDecorHwcLayer == null) {
return;
}
mScreenDecorHwcLayer.updateRoundedCornerExistenceAndSize(
mRoundedCornerResDelegate.getHasTop(),
mRoundedCornerResDelegate.getHasBottom(),
mRoundedCornerResDelegate.getTopRoundedSize().getWidth(),
mRoundedCornerResDelegate.getBottomRoundedSize().getWidth());
if (mDebug && mDebugRoundedCornerFactory.getHasProviders()) {
mScreenDecorHwcLayer.updateRoundedCornerExistenceAndSize(
mDebugRoundedCornerDelegate.getHasTop(),
mDebugRoundedCornerDelegate.getHasBottom(),
mDebugRoundedCornerDelegate.getTopRoundedSize().getWidth(),
mDebugRoundedCornerDelegate.getBottomRoundedSize().getWidth());
} else {
mScreenDecorHwcLayer.updateRoundedCornerExistenceAndSize(
mRoundedCornerResDelegate.getHasTop(),
mRoundedCornerResDelegate.getHasBottom(),
mRoundedCornerResDelegate.getTopRoundedSize().getWidth(),
mRoundedCornerResDelegate.getBottomRoundedSize().getWidth());
}
}
@VisibleForTesting

View File

@@ -0,0 +1,198 @@
/*
* Copyright (C) 2023 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.decor
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PixelFormat
import android.graphics.drawable.Drawable
import android.util.Size
import java.io.PrintWriter
/**
* Rounded corner delegate that handles incoming debug commands and can convert them to path
* drawables to be shown instead of the system-defined rounded corners.
*
* These debug corners are expected to supersede the system-defined corners
*/
class DebugRoundedCornerDelegate : RoundedCornerResDelegate {
override var hasTop: Boolean = false
private set
override var topRoundedDrawable: Drawable? = null
private set
override var topRoundedSize: Size = Size(0, 0)
private set
override var hasBottom: Boolean = false
private set
override var bottomRoundedDrawable: Drawable? = null
private set
override var bottomRoundedSize: Size = Size(0, 0)
private set
override var physicalPixelDisplaySizeRatio: Float = 1f
set(value) {
if (field == value) {
return
}
field = value
reloadMeasures()
}
var color: Int = Color.RED
set(value) {
if (field == value) {
return
}
field = value
paint.color = field
}
var paint =
Paint().apply {
color = Color.RED
style = Paint.Style.FILL
}
override fun updateDisplayUniqueId(newDisplayUniqueId: String?, newReloadToken: Int?) {
// nop -- debug corners draw the same on every display
}
fun applyNewDebugCorners(
topCorner: DebugRoundedCornerModel,
bottomCorner: DebugRoundedCornerModel,
) {
hasTop = true
topRoundedDrawable = topCorner.toPathDrawable(paint)
topRoundedSize = topCorner.size()
hasBottom = true
bottomRoundedDrawable = bottomCorner.toPathDrawable(paint)
bottomRoundedSize = bottomCorner.size()
}
/**
* Remove accumulated debug state by clearing out the drawables and setting [hasTop] and
* [hasBottom] to false.
*/
fun removeDebugState() {
hasTop = false
topRoundedDrawable = null
topRoundedSize = Size(0, 0)
hasBottom = false
bottomRoundedDrawable = null
bottomRoundedSize = Size(0, 0)
}
/**
* Scaling here happens when the display resolution is changed. This logic is exactly the same
* as in [RoundedCornerResDelegateImpl]
*/
private fun reloadMeasures() {
topRoundedDrawable?.let { topRoundedSize = Size(it.intrinsicWidth, it.intrinsicHeight) }
bottomRoundedDrawable?.let {
bottomRoundedSize = Size(it.intrinsicWidth, it.intrinsicHeight)
}
if (physicalPixelDisplaySizeRatio != 1f) {
if (topRoundedSize.width != 0) {
topRoundedSize =
Size(
(physicalPixelDisplaySizeRatio * topRoundedSize.width + 0.5f).toInt(),
(physicalPixelDisplaySizeRatio * topRoundedSize.height + 0.5f).toInt()
)
}
if (bottomRoundedSize.width != 0) {
bottomRoundedSize =
Size(
(physicalPixelDisplaySizeRatio * bottomRoundedSize.width + 0.5f).toInt(),
(physicalPixelDisplaySizeRatio * bottomRoundedSize.height + 0.5f).toInt()
)
}
}
}
fun dump(pw: PrintWriter) {
pw.println("DebugRoundedCornerDelegate state:")
pw.println(" hasTop=$hasTop")
pw.println(" hasBottom=$hasBottom")
pw.println(" topRoundedSize(w,h)=(${topRoundedSize.width},${topRoundedSize.height})")
pw.println(
" bottomRoundedSize(w,h)=(${bottomRoundedSize.width},${bottomRoundedSize.height})"
)
pw.println(" physicalPixelDisplaySizeRatio=$physicalPixelDisplaySizeRatio")
}
}
/** Encapsulates the data coming in from the command line args and turns into a [PathDrawable] */
data class DebugRoundedCornerModel(
val path: Path,
val width: Int,
val height: Int,
val scaleX: Float,
val scaleY: Float,
) {
fun size() = Size(width, height)
fun toPathDrawable(paint: Paint) =
PathDrawable(
path,
width,
height,
scaleX,
scaleY,
paint,
)
}
/**
* PathDrawable accepts paths from the command line via [DebugRoundedCornerModel], and renders them
* in the canvas provided by the screen decor rounded corner provider
*/
class PathDrawable(
val path: Path,
val width: Int,
val height: Int,
val scaleX: Float = 1f,
val scaleY: Float = 1f,
val paint: Paint,
) : Drawable() {
private var cf: ColorFilter? = null
override fun draw(canvas: Canvas) {
if (scaleX != 1f || scaleY != 1f) {
canvas.scale(scaleX, scaleY)
}
canvas.drawPath(path, paint)
}
override fun getIntrinsicHeight(): Int = height
override fun getIntrinsicWidth(): Int = width
override fun getOpacity(): Int = PixelFormat.OPAQUE
override fun setAlpha(alpha: Int) {}
override fun setColorFilter(colorFilter: ColorFilter?) {
cf = colorFilter
}
}

View File

@@ -62,6 +62,7 @@ import android.os.Handler;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.testing.TestableLooper.RunWithLooper;
import android.util.PathParser;
import android.util.Size;
import android.view.Display;
import android.view.DisplayCutout;
@@ -82,6 +83,7 @@ import com.android.systemui.biometrics.AuthController;
import com.android.systemui.decor.CornerDecorProvider;
import com.android.systemui.decor.CutoutDecorProviderFactory;
import com.android.systemui.decor.CutoutDecorProviderImpl;
import com.android.systemui.decor.DebugRoundedCornerModel;
import com.android.systemui.decor.DecorProvider;
import com.android.systemui.decor.DecorProviderFactory;
import com.android.systemui.decor.FaceScanningOverlayProviderImpl;
@@ -258,6 +260,8 @@ public class ScreenDecorationsTest extends SysuiTestCase {
}
});
mScreenDecorations.mDisplayInfo = mDisplayInfo;
// Make sure tests are never run starting in debug mode
mScreenDecorations.setDebug(false);
doReturn(1f).when(mScreenDecorations).getPhysicalPixelDisplaySizeRatio();
doNothing().when(mScreenDecorations).updateOverlayProviderViews(any());
@@ -1053,6 +1057,82 @@ public class ScreenDecorationsTest extends SysuiTestCase {
assertEquals(true, providers.get(1).getAlignedBounds().contains(BOUNDS_POSITION_BOTTOM));
}
@Test
public void testDebugRoundedCorners_noDeviceCornersSet() {
setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
null /* roundedTopDrawable */, null /* roundedBottomDrawable */,
0 /* roundedPadding */, false /* privacyDot */, false /* faceScanning */);
mScreenDecorations.start();
// No rounded corners exist at this point
verifyOverlaysExistAndAdded(false, false, false, false, View.VISIBLE);
// Path from rounded.xml, scaled by 10x to produce 80x80 corners
Path debugPath = PathParser.createPathFromPathData("M8,0H0v8C0,3.6,3.6,0,8,0z");
// WHEN debug corners are added to the delegate
DebugRoundedCornerModel debugCorner = new DebugRoundedCornerModel(
debugPath,
80,
80,
10f,
10f
);
mScreenDecorations.mDebugRoundedCornerDelegate
.applyNewDebugCorners(debugCorner, debugCorner);
// AND debug mode is entered
mScreenDecorations.setDebug(true);
mExecutor.runAllReady();
// THEN the debug corners provide decor
List<DecorProvider> providers = mScreenDecorations.getProviders(false);
assertEquals(4, providers.size());
// Top and bottom overlays contain the debug rounded corners
verifyOverlaysExistAndAdded(false, true, false, true, View.VISIBLE);
}
@Test
public void testDebugRoundedCornersRemoved_noDeviceCornersSet() {
// GIVEN a device with no rounded corners defined
setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
null /* roundedTopDrawable */, null /* roundedBottomDrawable */,
0 /* roundedPadding */, false /* privacyDot */, false /* faceScanning */);
mScreenDecorations.start();
// No rounded corners exist at this point
verifyOverlaysExistAndAdded(false, false, false, false, View.VISIBLE);
// Path from rounded.xml, scaled by 10x to produce 80x80 corners
Path debugPath = PathParser.createPathFromPathData("M8,0H0v8C0,3.6,3.6,0,8,0z");
// WHEN debug corners are added to the delegate
DebugRoundedCornerModel debugCorner = new DebugRoundedCornerModel(
debugPath,
80,
80,
10f,
10f
);
mScreenDecorations.mDebugRoundedCornerDelegate
.applyNewDebugCorners(debugCorner, debugCorner);
// AND debug mode is entered
mScreenDecorations.setDebug(true);
mExecutor.runAllReady();
// Top and bottom overlays contain the debug rounded corners
verifyOverlaysExistAndAdded(false, true, false, true, View.VISIBLE);
// WHEN debug is exited
mScreenDecorations.setDebug(false);
mExecutor.runAllReady();
// THEN the decor is removed
verifyOverlaysExistAndAdded(false, false, false, false, View.VISIBLE);
assertThat(mScreenDecorations.mDebugRoundedCornerDelegate.getHasBottom()).isFalse();
assertThat(mScreenDecorations.mDebugRoundedCornerDelegate.getHasTop()).isFalse();
}
@Test
public void testRegistration_From_NoOverlay_To_HasOverlays() {
doReturn(false).when(mScreenDecorations).hasOverlays();