Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d8e648ee authored by Evan Laird's avatar Evan Laird
Browse files

[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
parent eba0b0f0
Loading
Loading
Loading
Loading
+49 −13
Original line number Diff line number Diff line
@@ -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) {
            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,12 +1238,20 @@ public class ScreenDecorations implements CoreStartable, Dumpable {
        if (mScreenDecorHwcLayer == null) {
            return;
        }
        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
    protected void setSize(View view, Size pixelSize) {
+198 −0
Original line number Diff line number Diff line
/*
 * 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
    }
}
+80 −0
Original line number Diff line number Diff line
@@ -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();