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

Commit 6e4eccab authored by Milton Wu's avatar Milton Wu
Browse files

Re-land ScreenDecorations Cutout Provider

Move cutout to provider.

Bug: 232874879
Test: atest ScreenDecorationsTest CutoutDecorProviderFactoryTest \
      OverlayWindowTest
Test: fold/unfold/rotate/debugRoundedCornerSize/debugFlag on sw-layer
      devices
Test: adb shell dumpsys activity service com.android.systemui
Change-Id: I5f0b1e08ba7fee503b0926001cbdbd752edf2d48
parent f1fc5d21
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -131,7 +131,11 @@
    <!-- For StatusIconContainer to tag its icon views -->
    <item type="id" name="status_bar_view_state_tag" />

    <!-- Default display cutout on the physical top of screen -->
    <item type="id" name="display_cutout" />
    <item type="id" name="display_cutout_left" />
    <item type="id" name="display_cutout_right" />
    <item type="id" name="display_cutout_bottom" />

    <item type="id" name="row_tag_for_content_view" />

+71 −129
Original line number Diff line number Diff line
@@ -72,6 +72,7 @@ import com.android.settingslib.Utils;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.decor.CutoutDecorProviderFactory;
import com.android.systemui.decor.DecorProvider;
import com.android.systemui.decor.DecorProviderFactory;
import com.android.systemui.decor.DecorProviderKt;
@@ -118,6 +119,13 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
    private static final boolean VERBOSE = false;
    static final boolean DEBUG_COLOR = DEBUG_SCREENSHOT_ROUNDED_CORNERS;

    private static final int[] DISPLAY_CUTOUT_IDS = {
            R.id.display_cutout,
            R.id.display_cutout_left,
            R.id.display_cutout_right,
            R.id.display_cutout_bottom
    };

    private DisplayManager mDisplayManager;
    @VisibleForTesting
    protected boolean mIsRegistered;
@@ -139,13 +147,11 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
    protected RoundedCornerResDelegate mRoundedCornerResDelegate;
    @VisibleForTesting
    protected DecorProviderFactory mRoundedCornerFactory;
    private CutoutDecorProviderFactory mCutoutFactory;
    private int mProviderRefreshToken = 0;
    @VisibleForTesting
    protected OverlayWindow[] mOverlays = null;
    @VisibleForTesting
    @Nullable
    DisplayCutoutView[] mCutoutViews;
    @VisibleForTesting
    ViewGroup mScreenDecorHwcWindow;
    @VisibleForTesting
    ScreenDecorHwcLayer mScreenDecorHwcLayer;
@@ -187,18 +193,19 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
            return;
        }

        if (mCutoutViews == null) {
            Log.w(TAG, "DisplayCutoutView not initialized onApplyCameraProtection");
            return;
        int setProtectionCnt = 0;
        for (int id: DISPLAY_CUTOUT_IDS) {
            final View view = getOverlayView(id);
            if (!(view instanceof DisplayCutoutView)) {
                continue;
            }

        // Show the extra protection around the front facing camera if necessary
        for (DisplayCutoutView dcv : mCutoutViews) {
            // Check Null since not all mCutoutViews[pos] be inflated at the meanwhile
            if (dcv != null) {
            ++setProtectionCnt;
            final DisplayCutoutView dcv = (DisplayCutoutView) view;
            dcv.setProtection(protectionPath, bounds);
            dcv.enableShowProtection(true);
        }
        if (setProtectionCnt == 0) {
            Log.e(TAG, "CutoutView not initialized showCameraProtection");
        }
    }

@@ -219,16 +226,17 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
            return;
        }

        if (mCutoutViews == null) {
            Log.w(TAG, "DisplayCutoutView not initialized onHideCameraProtection");
            return;
        int setProtectionCnt = 0;
        for (int id: DISPLAY_CUTOUT_IDS) {
            final View view = getOverlayView(id);
            if (!(view instanceof DisplayCutoutView)) {
                continue;
            }
        // Go back to the regular anti-aliasing
        for (DisplayCutoutView dcv : mCutoutViews) {
            // Check Null since not all mCutoutViews[pos] be inflated at the meanwhile
            if (dcv != null) {
                dcv.enableShowProtection(false);
            ++setProtectionCnt;
            ((DisplayCutoutView) view).enableShowProtection(false);
        }
        if (setProtectionCnt == 0) {
            Log.e(TAG, "CutoutView not initialized hideCameraProtection");
        }
    }

@@ -335,6 +343,7 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
        decorProviders.addAll(mFaceScanningFactory.getProviders());
        if (!hasHwLayer) {
            decorProviders.addAll(mRoundedCornerFactory.getProviders());
            decorProviders.addAll(mCutoutFactory.getProviders());
        }
        return decorProviders;
    }
@@ -379,6 +388,7 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
        mRoundedCornerResDelegate.setPhysicalPixelDisplaySizeRatio(
                getPhysicalPixelDisplaySizeRatio());
        mRoundedCornerFactory = new RoundedCornerDecorProviderFactory(mRoundedCornerResDelegate);
        mCutoutFactory = getCutoutFactory();
        mHwcScreenDecorationSupport = mContext.getDisplay().getDisplayDecorationSupport();
        updateHwLayerRoundedCornerDrawable();
        setupDecorations();
@@ -483,18 +493,13 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
                if (needToUpdateProviderViews) {
                    updateOverlayProviderViews(null);
                } else {
                    updateOverlayProviderViews(new Integer[] { mFaceScanningViewId });
                }

                if (mCutoutViews != null) {
                    final int size = mCutoutViews.length;
                    for (int i = 0; i < size; i++) {
                        final DisplayCutoutView cutoutView = mCutoutViews[i];
                        if (cutoutView == null) {
                            continue;
                        }
                        cutoutView.onDisplayChanged(newUniqueId);
                    }
                    updateOverlayProviderViews(new Integer[] {
                            mFaceScanningViewId,
                            R.id.display_cutout,
                            R.id.display_cutout_left,
                            R.id.display_cutout_right,
                            R.id.display_cutout_bottom,
                    });
                }

                if (mScreenDecorHwcLayer != null) {
@@ -507,8 +512,9 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
        updateConfiguration();
    }

    @VisibleForTesting
    @Nullable
    private View getOverlayView(@IdRes int id) {
    View getOverlayView(@IdRes int id) {
        if (mOverlays == null) {
            return null;
        }
@@ -565,18 +571,18 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
                removeHwcOverlay();
            }

            final DisplayCutout cutout = getCutout();
            boolean[] hasCreatedOverlay = new boolean[BOUNDS_POSITION_LENGTH];
            final boolean shouldOptimizeVisibility = shouldOptimizeVisibility();
            for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
                if (shouldShowSwLayerCutout(i, cutout)
                        || shouldShowSwLayerFaceScan(i, cutout)
                        || shouldShowSwLayerRoundedCorner(i, cutout)
                        || shouldShowSwLayerPrivacyDot(i, cutout)) {
            Integer bound;
            while ((bound = DecorProviderKt.getProperBound(decorProviders)) != null) {
                hasCreatedOverlay[bound] = true;
                Pair<List<DecorProvider>, List<DecorProvider>> pair =
                            DecorProviderKt.partitionAlignedBound(decorProviders, i);
                        DecorProviderKt.partitionAlignedBound(decorProviders, bound);
                decorProviders = pair.getSecond();
                    createOverlay(i, pair.getFirst(), shouldOptimizeVisibility);
                } else {
                createOverlay(bound, pair.getFirst(), shouldOptimizeVisibility);
            }
            for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
                if (!hasCreatedOverlay[i]) {
                    removeOverlay(i);
                }
            }
@@ -639,9 +645,10 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
        }
    }

    @VisibleForTesting
    DisplayCutout getCutout() {
        return mContext.getDisplay().getCutout();
    // For unit test to override
    protected CutoutDecorProviderFactory getCutoutFactory() {
        return new CutoutDecorProviderFactory(mContext.getResources(),
                mContext.getDisplay());
    }

    @VisibleForTesting
@@ -731,16 +738,6 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
        overlayView.setAlpha(0);
        overlayView.setForceDarkAllowed(false);

        // Only show cutout in mOverlays when hwc doesn't support screen decoration
        if (mHwcScreenDecorationSupport == null) {
            if (mCutoutViews == null) {
                mCutoutViews = new DisplayCutoutView[BOUNDS_POSITION_LENGTH];
            }
            mCutoutViews[pos] = new DisplayCutoutView(mContext, pos);
            overlayView.addView(mCutoutViews[pos]);
            mCutoutViews[pos].updateRotation(mRotation);
        }

        mWindowManager.addView(overlayView, getWindowLayoutParams(pos));

        overlayView.addOnLayoutChangeListener(new OnLayoutChangeListener() {
@@ -920,6 +917,7 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
    }

    private void setupCameraListener() {
        // TODO(b/238143614) Support dual screen camera protection
        Resources res = mContext.getResources();
        boolean enabled = res.getBoolean(R.bool.config_enableDisplayCutoutProtection);
        if (enabled) {
@@ -948,27 +946,12 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
            mTintColor = Color.RED;
        }

        if (mOverlays == null) {
            return;
        }

        for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
            if (mOverlays[i] == null) {
                continue;
            }
            final ViewGroup overlayView = mOverlays[i].getRootView();
            final int size = overlayView.getChildCount();
            View child;
            for (int j = 0; j < size; j++) {
                child = overlayView.getChildAt(j);
                if (child instanceof DisplayCutoutView && child.getId() == R.id.display_cutout) {
                    ((DisplayCutoutView) child).setColor(mTintColor);
                }
            }
        }

        updateOverlayProviderViews(new Integer[] {
                mFaceScanningViewId,
                R.id.display_cutout,
                R.id.display_cutout_left,
                R.id.display_cutout_right,
                R.id.display_cutout_bottom,
                R.id.rounded_corner_top_left,
                R.id.rounded_corner_top_right,
                R.id.rounded_corner_bottom_left,
@@ -1093,15 +1076,6 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
                updateHwLayerRoundedCornerDrawable();
            }
            updateLayoutParams();
            // update cutout view rotation
            if (mCutoutViews != null) {
                for (final DisplayCutoutView cutoutView: mCutoutViews) {
                    if (cutoutView == null) {
                        continue;
                    }
                    cutoutView.updateRotation(mRotation);
                }
            }

            // update all provider views inside overlay
            updateOverlayProviderViews(null);
@@ -1120,46 +1094,6 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
        return mRoundedCornerFactory.getHasProviders();
    }

    private boolean isDefaultShownOverlayPos(@BoundsPosition int pos,
            @Nullable DisplayCutout cutout) {
        // for cutout is null or cutout with only waterfall.
        final boolean emptyBoundsOrWaterfall = cutout == null || cutout.isBoundsEmpty();
        // Shows rounded corner on left and right overlays only when there is no top or bottom
        // cutout.
        final int rotatedTop = getBoundPositionFromRotation(BOUNDS_POSITION_TOP, mRotation);
        final int rotatedBottom = getBoundPositionFromRotation(BOUNDS_POSITION_BOTTOM, mRotation);
        if (emptyBoundsOrWaterfall || !cutout.getBoundingRectsAll()[rotatedTop].isEmpty()
                || !cutout.getBoundingRectsAll()[rotatedBottom].isEmpty()) {
            return pos == BOUNDS_POSITION_TOP || pos == BOUNDS_POSITION_BOTTOM;
        } else {
            return pos == BOUNDS_POSITION_LEFT || pos == BOUNDS_POSITION_RIGHT;
        }
    }

    private boolean shouldShowSwLayerRoundedCorner(@BoundsPosition int pos,
            @Nullable DisplayCutout cutout) {
        return hasRoundedCorners() && isDefaultShownOverlayPos(pos, cutout)
                && mHwcScreenDecorationSupport == null;
    }

    private boolean shouldShowSwLayerPrivacyDot(@BoundsPosition int pos,
            @Nullable DisplayCutout cutout) {
        return isPrivacyDotEnabled() && isDefaultShownOverlayPos(pos, cutout);
    }

    private boolean shouldShowSwLayerFaceScan(@BoundsPosition int pos,
            @Nullable DisplayCutout cutout) {
        return mFaceScanningFactory.getHasProviders() && isDefaultShownOverlayPos(pos, cutout);
    }

    private boolean shouldShowSwLayerCutout(@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()
                && mHwcScreenDecorationSupport == null);
    }

    private boolean shouldOptimizeVisibility() {
        return (isPrivacyDotEnabled() || mFaceScanningFactory.getHasProviders())
                && (mHwcScreenDecorationSupport != null
@@ -1168,7 +1102,7 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
    }

    private boolean shouldDrawCutout() {
        return shouldDrawCutout(mContext);
        return mCutoutFactory.getHasProviders();
    }

    static boolean shouldDrawCutout(Context context) {
@@ -1284,7 +1218,6 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab

            paint.setColor(mColor);
            paint.setStyle(Paint.Style.FILL);
            setId(R.id.display_cutout);
            if (DEBUG) {
                getViewTreeObserver().addOnDrawListener(() -> Log.i(TAG,
                        getWindowTitleByPos(pos) + " drawn in rot " + mRotation));
@@ -1292,6 +1225,9 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab
        }

        public void setColor(int color) {
            if (color == mColor) {
                return;
            }
            mColor = color;
            paint.setColor(mColor);
            invalidate();
@@ -1299,6 +1235,12 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab

        @Override
        public void updateRotation(int rotation) {
            // updateRotation() is called inside CutoutDecorProviderImpl::onReloadResAndMeasure()
            // during onDisplayChanged. In order to prevent reloading cutout info in super class,
            // check mRotation at first
            if (rotation == mRotation) {
                return;
            }
            mRotation = rotation;
            super.updateRotation(rotation);
        }
+60 −0
Original line number Diff line number Diff line
/*
 * 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.decor

import android.content.res.Resources
import android.util.Log
import android.view.Display
import android.view.DisplayCutout
import android.view.DisplayInfo

class CutoutDecorProviderFactory constructor(
    private val res: Resources,
    private val display: Display?,
) : DecorProviderFactory() {

    val displayInfo = DisplayInfo()

    override val hasProviders: Boolean
        get() {
            display?.getDisplayInfo(displayInfo) ?: run {
                Log.w(TAG, "display is null, can't update displayInfo")
            }
            return DisplayCutout.getFillBuiltInDisplayCutout(res, displayInfo.uniqueId)
        }

    override val providers: List<DecorProvider>
        get() {
            if (!hasProviders) {
                return emptyList()
            }

            return ArrayList<DecorProvider>().also { list ->
                // We need to update displayInfo before using it, but it has already updated during
                // accessing hasProviders field
                displayInfo.displayCutout?.getBoundBaseOnCurrentRotation()?.let { bounds ->
                    for (bound in bounds) {
                        list.add(
                            CutoutDecorProviderImpl(bound.baseOnRotation0(displayInfo.rotation))
                        )
                    }
                }
            }
        }
}

private const val TAG = "CutoutDecorProviderFactory"
+65 −0
Original line number Diff line number Diff line
/*
 * 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.decor

import android.content.Context
import android.view.DisplayCutout
import android.view.Surface
import android.view.View
import android.view.ViewGroup
import com.android.systemui.R
import com.android.systemui.ScreenDecorations.DisplayCutoutView

class CutoutDecorProviderImpl(
    @DisplayCutout.BoundsPosition override val alignedBound: Int
) : BoundDecorProvider() {

    override val viewId: Int = when (alignedBound) {
        DisplayCutout.BOUNDS_POSITION_TOP -> R.id.display_cutout
        DisplayCutout.BOUNDS_POSITION_LEFT -> R.id.display_cutout_left
        DisplayCutout.BOUNDS_POSITION_RIGHT -> R.id.display_cutout_right
        else -> R.id.display_cutout_bottom
    }

    override fun inflateView(
        context: Context,
        parent: ViewGroup,
        @Surface.Rotation rotation: Int,
        tintColor: Int
    ): View {
        return DisplayCutoutView(context, alignedBound).also { view ->
            view.id = viewId
            view.setColor(tintColor)
            parent.addView(view)
            view.updateRotation(rotation)
        }
    }

    override fun onReloadResAndMeasure(
        view: View,
        reloadToken: Int,
        @Surface.Rotation rotation: Int,
        tintColor: Int,
        displayUniqueId: String?
    ) {
        (view as? DisplayCutoutView)?.let { cutoutView ->
            cutoutView.setColor(tintColor)
            cutoutView.updateRotation(rotation)
            cutoutView.onDisplayChanged(displayUniqueId)
        }
    }
}
+60 −10
Original line number Diff line number Diff line
@@ -32,7 +32,7 @@ abstract class DecorProvider {
    abstract val viewId: Int

    /** The number of total aligned bounds */
    val numOfAlignedEdge: Int
    val numOfAlignedBound: Int
        get() = alignedBounds.size

    /** The aligned bounds for the view which is created through inflateView() */
@@ -57,16 +57,8 @@ abstract class DecorProvider {
        @Surface.Rotation rotation: Int,
        tintColor: Int
    ): View
}

/**
 * Split list to 2 list, and return it back as Pair<>. The providers on the first list contains this
 * alignedBound element. The providers on the second list do not contain this alignedBound element
 */
fun List<DecorProvider>.partitionAlignedBound(
    @DisplayCutout.BoundsPosition alignedBound: Int
): Pair<List<DecorProvider>, List<DecorProvider>> {
    return partition { it.alignedBounds.contains(alignedBound) }
    override fun toString() = "${javaClass.simpleName}{alignedBounds=$alignedBounds}"
}

/**
@@ -94,3 +86,61 @@ abstract class BoundDecorProvider : DecorProvider() {
        listOf(alignedBound)
    }
}

/**
 * Split list to 2 sub-lists, and return it back as Pair<>. The providers on the first list contains
 * this alignedBound element. The providers on the second list do not contain this alignedBound
 * element.
 */
fun List<DecorProvider>.partitionAlignedBound(
    @DisplayCutout.BoundsPosition alignedBound: Int
): Pair<List<DecorProvider>, List<DecorProvider>> {
    return partition { it.alignedBounds.contains(alignedBound) }
}

/**
 * Get the proper bound from DecorProvider list
 * Time complexity: O(N), N is the number of providers
 *
 * Choose order
 * 1. Return null if list is empty
 * 2. If list contains BoundDecorProvider, return its alignedBound[0] because it is a must-have
 *    bound
 * 3. Return the bound with most DecorProviders
 */
fun List<DecorProvider>.getProperBound(): Int? {
    // Return null if list is empty
    if (isEmpty()) {
        return null
    }

    // Choose alignedBounds[0] of BoundDecorProvider if any
    val singleBoundProvider = firstOrNull { it.numOfAlignedBound == 1 }
    if (singleBoundProvider != null) {
        return singleBoundProvider.alignedBounds[0]
    }

    // Return the bound with most DecorProviders
    val boundCount = intArrayOf(0, 0, 0, 0)
    for (provider in this) {
        for (bound in provider.alignedBounds) {
            boundCount[bound]++
        }
    }
    var maxCount = 0
    var maxCountBound: Int? = null
    val bounds = arrayOf(
        // Put top and bottom at first to get the highest priority to be chosen
        DisplayCutout.BOUNDS_POSITION_TOP,
        DisplayCutout.BOUNDS_POSITION_BOTTOM,
        DisplayCutout.BOUNDS_POSITION_LEFT,
        DisplayCutout.BOUNDS_POSITION_RIGHT
    )
    for (bound in bounds) {
        if (boundCount[bound] > maxCount) {
            maxCountBound = bound
            maxCount = boundCount[bound]
        }
    }
    return maxCountBound
}
Loading