diff options
3 files changed, 327 insertions, 13 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java index 892b9dc7f08e..67d4a2e25051 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java @@ -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 diff --git a/packages/SystemUI/src/com/android/systemui/decor/DebugRoundedCornerDelegate.kt b/packages/SystemUI/src/com/android/systemui/decor/DebugRoundedCornerDelegate.kt new file mode 100644 index 000000000000..4069bc7d73d0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/decor/DebugRoundedCornerDelegate.kt @@ -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 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java index a6006a1e6a48..79c87cfd1f3e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java @@ -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()); @@ -1054,6 +1058,82 @@ public class ScreenDecorationsTest extends SysuiTestCase { } @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(); mScreenDecorations.start(); |