/*
 * Copyright (C) 2017 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 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.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 static com.android.systemui.util.DumpUtilsKt.asIndenting;

import android.annotation.IdRes;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.hardware.graphics.common.AlphaInterpretation;
import android.hardware.graphics.common.DisplayDecorationSupport;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.SystemProperties;
import android.os.Trace;
import android.provider.Settings.Secure;
import android.util.DisplayUtils;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Size;
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.View;
import android.view.View.OnLayoutChangeListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.widget.FrameLayout;

import androidx.annotation.VisibleForTesting;

import com.android.internal.util.Preconditions;
import com.android.systemui.biometrics.data.repository.FacePropertyRepository;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.decor.CutoutDecorProviderFactory;
import com.android.systemui.decor.DebugRoundedCornerDelegate;
import com.android.systemui.decor.DebugRoundedCornerModel;
import com.android.systemui.decor.DecorProvider;
import com.android.systemui.decor.DecorProviderFactory;
import com.android.systemui.decor.DecorProviderKt;
import com.android.systemui.decor.FaceScanningProviderFactory;
import com.android.systemui.decor.OverlayWindow;
import com.android.systemui.decor.PrivacyDotDecorProviderFactory;
import com.android.systemui.decor.RoundedCornerDecorProviderFactory;
import com.android.systemui.decor.RoundedCornerResDelegateImpl;
import com.android.systemui.decor.ScreenDecorCommand;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.log.ScreenDecorationsLogger;
import com.android.systemui.qs.UserSettingObserver;
import com.android.systemui.res.R;
import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.domain.interactor.ShadeInteractor;
import com.android.systemui.statusbar.commandline.CommandRegistry;
import com.android.systemui.statusbar.events.PrivacyDotViewController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.util.concurrency.DelayableExecutor;
import com.android.systemui.util.kotlin.JavaAdapter;
import com.android.systemui.util.settings.SecureSettings;

import dalvik.annotation.optimization.NeverCompile;

import kotlin.Pair;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import javax.inject.Inject;

/**
 * An overlay that draws screen decorations in software (e.g for rounded corners or display cutout)
 * for antialiasing and emulation purposes.
 */
@SysUISingleton
public class ScreenDecorations implements
        CoreStartable, ConfigurationController.ConfigurationListener {
    private static final boolean DEBUG_LOGGING = false;
    private static final String TAG = "ScreenDecorations";

    // Provide a way for factory to disable ScreenDecorations to run the Display tests.
    private static final boolean DEBUG_DISABLE_SCREEN_DECORATIONS =
            SystemProperties.getBoolean("debug.disable_screen_decorations", false);
    private static final boolean DEBUG_SCREENSHOT_ROUNDED_CORNERS =
            SystemProperties.getBoolean("debug.screenshot_rounded_corners", false);

    private static final boolean sToolkitSetFrameRateReadOnly =
            android.view.flags.Flags.toolkitSetFrameRateReadOnly();
    private boolean mDebug = DEBUG_SCREENSHOT_ROUNDED_CORNERS;
    private int mDebugColor = Color.RED;

    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 final ScreenDecorationsLogger mLogger;

    private DisplayTracker mDisplayTracker;
    @VisibleForTesting
    protected boolean mIsRegistered;
    private final Context mContext;
    private final CommandRegistry mCommandRegistry;
    private final SecureSettings mSecureSettings;
    @VisibleForTesting
    DisplayTracker.Callback mDisplayListener;
    private CameraAvailabilityListener mCameraListener;
    private final UserTracker mUserTracker;
    private final PrivacyDotViewController mDotViewController;
    private final DecorProviderFactory mDotFactory;
    private final FaceScanningProviderFactory mFaceScanningFactory;
    private final CameraProtectionLoader mCameraProtectionLoader;
    private final ShadeInteractor mShadeInteractor;
    private final KeyguardInteractor mKeyguardInteractor;
    public final int mFaceScanningViewId;

    @VisibleForTesting
    protected RoundedCornerResDelegateImpl mRoundedCornerResDelegate;
    @VisibleForTesting
    protected DecorProviderFactory mRoundedCornerFactory;
    @VisibleForTesting
    protected DebugRoundedCornerDelegate mDebugRoundedCornerDelegate =
            new DebugRoundedCornerDelegate();
    protected DecorProviderFactory mDebugRoundedCornerFactory;
    private CutoutDecorProviderFactory mCutoutFactory;
    private int mProviderRefreshToken = 0;
    @VisibleForTesting
    protected OverlayWindow[] mOverlays = null;
    @VisibleForTesting
    ViewGroup mScreenDecorHwcWindow;
    @VisibleForTesting
    ScreenDecorHwcLayer mScreenDecorHwcLayer;
    private WindowManager mWindowManager;
    private int mRotation;
    private UserSettingObserver mColorInversionSetting;
    private DelayableExecutor mExecutor;
    private Handler mHandler;
    boolean mPendingConfigChange;
    @VisibleForTesting
    String mDisplayUniqueId;
    private int mTintColor = Color.BLACK;
    @VisibleForTesting
    protected DisplayDecorationSupport mHwcScreenDecorationSupport;
    private final Point mDisplaySize = new Point();
    @VisibleForTesting
    protected DisplayInfo mDisplayInfo = new DisplayInfo();
    private DisplayCutout mDisplayCutout;
    private boolean mPendingManualConfigUpdate;

    private FacePropertyRepository mFacePropertyRepository;
    private JavaAdapter mJavaAdapter;

    @VisibleForTesting
    protected void showCameraProtection(@NonNull Path protectionPath, @NonNull Rect bounds) {
        if (mDebug || mFaceScanningFactory.shouldShowFaceScanningAnim()) {
            DisplayCutoutView overlay = (DisplayCutoutView) getOverlayView(
                    mFaceScanningViewId);
            if (overlay != null) {
                mLogger.cameraProtectionBoundsForScanningOverlay(bounds);
                overlay.setProtection(protectionPath, bounds);
                overlay.enableShowProtection(true);
                updateOverlayWindowVisibilityIfViewExists(
                        overlay.findViewById(mFaceScanningViewId));
                // immediately return, bc FaceScanningOverlay also renders the camera
                // protection, so we don't need to show the camera protection in
                // mScreenDecorHwcLayer or mCutoutViews
                return;
            }
        }

        if (mScreenDecorHwcLayer != null) {
            mLogger.hwcLayerCameraProtectionBounds(bounds);
            mScreenDecorHwcLayer.setProtection(protectionPath, bounds);
            mScreenDecorHwcLayer.enableShowProtection(true);
            return;
        }

        int setProtectionCnt = 0;
        for (int id: DISPLAY_CUTOUT_IDS) {
            final View view = getOverlayView(id);
            if (!(view instanceof DisplayCutoutView)) {
                continue;
            }
            ++setProtectionCnt;
            final DisplayCutoutView dcv = (DisplayCutoutView) view;
            mLogger.dcvCameraBounds(id, bounds);
            dcv.setProtection(protectionPath, bounds);
            dcv.enableShowProtection(true);
        }
        if (setProtectionCnt == 0) {
            mLogger.cutoutViewNotInitialized();
        }
    }

    @VisibleForTesting
    protected void hideCameraProtection() {
        FaceScanningOverlay faceScanningOverlay =
                (FaceScanningOverlay) getOverlayView(mFaceScanningViewId);
        if (faceScanningOverlay != null) {
            faceScanningOverlay.setHideOverlayRunnable(() -> {
                Trace.beginSection("ScreenDecorations#hideOverlayRunnable");
                updateOverlayWindowVisibilityIfViewExists(
                        faceScanningOverlay.findViewById(mFaceScanningViewId));
                Trace.endSection();
            });
            faceScanningOverlay.enableShowProtection(false);
        }

        if (mScreenDecorHwcLayer != null) {
            mScreenDecorHwcLayer.enableShowProtection(false);
            return;
        }

        int setProtectionCnt = 0;
        for (int id: DISPLAY_CUTOUT_IDS) {
            final View view = getOverlayView(id);
            if (!(view instanceof DisplayCutoutView)) {
                continue;
            }
            ++setProtectionCnt;
            ((DisplayCutoutView) view).enableShowProtection(false);
        }
        if (setProtectionCnt == 0) {
            Log.e(TAG, "CutoutView not initialized hideCameraProtection");
        }
    }

    private CameraAvailabilityListener.CameraTransitionCallback mCameraTransitionCallback =
            new CameraAvailabilityListener.CameraTransitionCallback() {
        @Override
        public void onApplyCameraProtection(@NonNull Path protectionPath, @NonNull Rect bounds) {
            mLogger.cameraProtectionEvent("onApplyCameraProtection");
            showCameraProtection(protectionPath, bounds);
        }

        @Override
        public void onHideCameraProtection() {
            mLogger.cameraProtectionEvent("onHideCameraProtection");
            hideCameraProtection();
        }
    };

    @VisibleForTesting
    PrivacyDotViewController.ShowingListener mPrivacyDotShowingListener =
            new PrivacyDotViewController.ShowingListener() {
        @Override
        public void onPrivacyDotShown(@Nullable View v) {
            updateOverlayWindowVisibilityIfViewExists(v);
        }

        @Override
        public void onPrivacyDotHidden(@Nullable View v) {
            updateOverlayWindowVisibilityIfViewExists(v);
        }
    };

    @VisibleForTesting
    protected void updateOverlayWindowVisibilityIfViewExists(@Nullable View view) {
        if (view == null) {
            return;
        }
        mExecutor.execute(() -> {
            // We don't need to control the window visibility if rounded corners or cutout is drawn
            // on sw layer since the overlay windows are always visible in this case.
            if (mOverlays == null || !shouldOptimizeVisibility()) {
                return;
            }
            Trace.beginSection("ScreenDecorations#updateOverlayWindowVisibilityIfViewExists");
            for (final OverlayWindow overlay : mOverlays) {
                if (overlay == null) {
                    continue;
                }
                if (overlay.getView(view.getId()) != null) {
                    overlay.getRootView().setVisibility(getWindowVisibility(overlay, true));
                    Trace.endSection();
                    return;
                }
            }
            Trace.endSection();
        });
    }

    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
    public ScreenDecorations(
            Context context,
            SecureSettings secureSettings,
            CommandRegistry commandRegistry,
            UserTracker userTracker,
            DisplayTracker displayTracker,
            PrivacyDotViewController dotViewController,
            PrivacyDotDecorProviderFactory dotFactory,
            FaceScanningProviderFactory faceScanningFactory,
            ScreenDecorationsLogger logger,
            FacePropertyRepository facePropertyRepository,
            JavaAdapter javaAdapter,
            CameraProtectionLoader cameraProtectionLoader,
            WindowManager windowManager,
            ShadeInteractor shadeInteractor,
            KeyguardInteractor keyguardInteractor,
            @ScreenDecorationsThread Handler handler,
            @ScreenDecorationsThread DelayableExecutor executor) {
        mContext = context;
        mSecureSettings = secureSettings;
        mCommandRegistry = commandRegistry;
        mUserTracker = userTracker;
        mDisplayTracker = displayTracker;
        mDotViewController = dotViewController;
        mDotFactory = dotFactory;
        mFaceScanningFactory = faceScanningFactory;
        mCameraProtectionLoader = cameraProtectionLoader;
        mShadeInteractor = shadeInteractor;
        mKeyguardInteractor = keyguardInteractor;
        mFaceScanningViewId = com.android.systemui.res.R.id.face_scanning_anim;
        mLogger = logger;
        mFacePropertyRepository = facePropertyRepository;
        mJavaAdapter = javaAdapter;
        mWindowManager = windowManager;
        mHandler = handler;
        mExecutor = executor;
    }

    private final ScreenDecorCommand.Callback mScreenDecorCommandCallback = (cmd, pw) -> {
        // If we are exiting debug mode, we can set it (false) and bail, otherwise we will
        // ensure that debug mode is set
        if (cmd.getDebug() != null && !cmd.getDebug()) {
            setDebug(false);
            return;
        } else {
            // setDebug is idempotent
            setDebug(true);
        }

        if (cmd.getColor() != null) {
            mDebugColor = cmd.getColor();
            mExecutor.execute(() -> {
                if (mScreenDecorHwcLayer != null) {
                    mScreenDecorHwcLayer.setDebugColor(cmd.getColor());
                }
                updateColorInversionDefault();
            });
        }

        DebugRoundedCornerModel roundedTop = null;
        DebugRoundedCornerModel roundedBottom = null;
        if (cmd.getRoundedTop() != null) {
            roundedTop = cmd.getRoundedTop().toRoundedCornerDebugModel();
        }
        if (cmd.getRoundedBottom() != null) {
            roundedBottom = cmd.getRoundedBottom().toRoundedCornerDebugModel();
        }
        if (roundedTop != null || roundedBottom != null) {
            mDebugRoundedCornerDelegate.applyNewDebugCorners(roundedTop, roundedBottom);
            mExecutor.execute(() -> {
                removeAllOverlays();
                removeHwcOverlay();
                setupDecorations();
            });
        }

        if (cmd.getFaceAuthScreen() != null) {
            mExecutor.execute(() -> {
                debugTriggerFaceAuth(cmd.getFaceAuthScreen());
            });
        }
    };

    @Override
    public void start() {
        if (DEBUG_DISABLE_SCREEN_DECORATIONS) {
            Log.i(TAG, "ScreenDecorations is disabled");
            return;
        }
        mExecutor.execute(this::startOnScreenDecorationsThread);
        mCommandRegistry.registerCommand(ScreenDecorCommand.SCREEN_DECOR_CMD_NAME,
                () -> new ScreenDecorCommand(mScreenDecorCommandCallback));
    }

    /**
     * Change the value of {@link ScreenDecorations#mDebug}. This operation is heavyweight, since
     * it requires essentially re-init-ing this screen decorations process with the debug
     * information taken into account.
     */
    @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
            removeAllOverlays();
            removeHwcOverlay();
            startOnScreenDecorationsThread();
            updateColorInversionDefault();
        });
    }

    private boolean isPrivacyDotEnabled() {
        return mDotFactory.getHasProviders();
    }

    @NonNull
    @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;
    }

    /**
     * Check that newProviders is the same list with decorProviders inside mOverlay.
     * @param newProviders expected comparing DecorProviders
     * @return true if same provider list
     */
    @VisibleForTesting
    boolean hasSameProviders(@NonNull List<DecorProvider> newProviders) {
        final ArrayList<Integer> overlayViewIds = new ArrayList<>();
        if (mOverlays != null) {
            for (OverlayWindow overlay : mOverlays) {
                if (overlay == null) {
                    continue;
                }
                overlayViewIds.addAll(overlay.getViewIds());
            }
        }
        if (overlayViewIds.size() != newProviders.size()) {
            return false;
        }

        for (DecorProvider provider: newProviders) {
            if (!overlayViewIds.contains(provider.getViewId())) {
                return false;
            }
        }
        return true;
    }

    private void startOnScreenDecorationsThread() {
        Trace.beginSection("ScreenDecorations#startOnScreenDecorationsThread");
        mContext.getDisplay().getDisplayInfo(mDisplayInfo);
        mRotation = mDisplayInfo.rotation;
        mDisplaySize.x = mDisplayInfo.getNaturalWidth();
        mDisplaySize.y = mDisplayInfo.getNaturalHeight();
        mDisplayUniqueId = mDisplayInfo.uniqueId;
        mDisplayCutout = mDisplayInfo.displayCutout;
        mRoundedCornerResDelegate =
                new RoundedCornerResDelegateImpl(mContext.getResources(), mDisplayUniqueId);
        mRoundedCornerResDelegate.setPhysicalPixelDisplaySizeRatio(
                getPhysicalPixelDisplaySizeRatio());
        mRoundedCornerFactory = new RoundedCornerDecorProviderFactory(mRoundedCornerResDelegate);
        mDebugRoundedCornerFactory =
                new RoundedCornerDecorProviderFactory(mDebugRoundedCornerDelegate);
        mCutoutFactory = getCutoutFactory();
        mHwcScreenDecorationSupport = mContext.getDisplay().getDisplayDecorationSupport();
        updateHwLayerRoundedCornerDrawable();
        setupDecorations();
        setupCameraListener();

        mDisplayListener = new DisplayTracker.Callback() {
            @Override
            public void onDisplayChanged(int displayId) {
                mContext.getDisplay().getDisplayInfo(mDisplayInfo);
                final int newRotation = mDisplayInfo.rotation;
                if ((mOverlays != null || mScreenDecorHwcWindow != null)
                        && (mRotation != newRotation
                        || displaySizeChanged(mDisplaySize, mDisplayInfo))) {
                    final Point newSize = new Point();
                    newSize.x = mDisplayInfo.getNaturalWidth();
                    newSize.y = mDisplayInfo.getNaturalHeight();
                    // We cannot immediately update the orientation. Otherwise
                    // WindowManager is still deferring layout until it has finished dispatching
                    // the config changes, which may cause divergence between what we draw
                    // (new orientation), and where we are placed on the screen (old orientation).
                    // Instead we wait until either:
                    // - we are trying to redraw. This because WM resized our window and told us to.
                    // - the config change has been dispatched, so WM is no longer deferring layout.
                    mPendingConfigChange = true;
                    if (mRotation != newRotation) {
                        mLogger.logRotationChangeDeferred(mRotation, newRotation);
                    }
                    if (!mDisplaySize.equals(newSize)) {
                        mLogger.logDisplaySizeChanged(mDisplaySize, newSize);
                    }

                    if (mOverlays != null) {
                        for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
                            if (mOverlays[i] != null) {
                                final ViewGroup overlayView = mOverlays[i].getRootView();
                                overlayView.getViewTreeObserver().addOnPreDrawListener(
                                        new RestartingPreDrawListener(
                                                overlayView, i, newRotation, newSize));
                            }
                        }
                    }

                    if (mScreenDecorHwcWindow != null) {
                        mScreenDecorHwcWindow.getViewTreeObserver().addOnPreDrawListener(
                                new RestartingPreDrawListener(
                                        mScreenDecorHwcWindow,
                                        -1, // Pass -1 for views with no specific position.
                                        newRotation, newSize));
                    }
                    if (mScreenDecorHwcLayer != null) {
                        mScreenDecorHwcLayer.pendingConfigChange = true;
                    }
                }

                final String newUniqueId = mDisplayInfo.uniqueId;
                if (!Objects.equals(newUniqueId, mDisplayUniqueId)) {
                    mDisplayUniqueId = newUniqueId;
                    final DisplayDecorationSupport newScreenDecorationSupport =
                            mContext.getDisplay().getDisplayDecorationSupport();

                    mRoundedCornerResDelegate.updateDisplayUniqueId(newUniqueId, null);

                    // When providers or the value of mSupportHwcScreenDecoration is changed,
                    // re-setup the whole screen decoration.
                    if (!hasSameProviders(getProviders(newScreenDecorationSupport != null))
                            || !eq(newScreenDecorationSupport, mHwcScreenDecorationSupport)) {
                        mHwcScreenDecorationSupport = newScreenDecorationSupport;
                        removeAllOverlays();
                        setupDecorations();
                        return;
                    }

                    if (mPendingManualConfigUpdate) {
                        mPendingManualConfigUpdate = false;
                        onConfigChanged(mContext.getResources().getConfiguration());
                    }
                }
            }
        };
        mDisplayTracker.addDisplayChangeCallback(mDisplayListener, new HandlerExecutor(mHandler));
        updateConfiguration();
        mJavaAdapter.alwaysCollectFlow(mFacePropertyRepository.getSensorLocation(),
                this::onFaceSensorLocationChanged);
        Trace.endSection();
    }

    @VisibleForTesting
    @Nullable
    View getOverlayView(@IdRes int id) {
        if (mOverlays == null) {
            return null;
        }

        for (final OverlayWindow overlay : mOverlays) {
            if (overlay == null) {
                continue;
            }
            final View view = overlay.getView(id);
            if (view != null) {
                return view;
            }
        }
        return null;
    }

    private void removeRedundantOverlayViews(@NonNull List<DecorProvider> decorProviders) {
        if (mOverlays == null) {
            return;
        }
        int[] viewIds = decorProviders.stream().mapToInt(DecorProvider::getViewId).toArray();
        for (final OverlayWindow overlay : mOverlays) {
            if (overlay == null) {
                continue;
            }
            overlay.removeRedundantViews(viewIds);
        }
    }

    private void removeOverlayView(@IdRes int id) {
        if (mOverlays == null) {
            return;
        }

        for (final OverlayWindow overlay : mOverlays) {
            if (overlay == null) {
                continue;
            }

            overlay.removeView(id);
        }
    }

    private void debugTriggerFaceAuth(int screen) {
        DisplayCutoutView overlay = (DisplayCutoutView) getOverlayView(
                mFaceScanningViewId);
        if (overlay != null) {
            overlay.setDebug(true);
            mCameraListener.debugFaceAuth(screen);
        }
    }

    private void setupDecorations() {
        Trace.beginSection("ScreenDecorations#setupDecorations");
        setupDecorationsInner();
        Trace.endSection();
    }

    private void setupDecorationsInner() {
        if (hasRoundedCorners() || shouldDrawCutout() || isPrivacyDotEnabled()
                || mFaceScanningFactory.getHasProviders()) {

            List<DecorProvider> decorProviders = getProviders(mHwcScreenDecorationSupport != null);
            removeRedundantOverlayViews(decorProviders);

            if (mHwcScreenDecorationSupport != null) {
                createHwcOverlay();
            } else {
                removeHwcOverlay();
            }

            boolean[] hasCreatedOverlay = new boolean[BOUNDS_POSITION_LENGTH];
            final boolean shouldOptimizeVisibility = shouldOptimizeVisibility();
            Integer bound;
            while ((bound = DecorProviderKt.getProperBound(decorProviders)) != null) {
                hasCreatedOverlay[bound] = true;
                Pair<List<DecorProvider>, List<DecorProvider>> pair =
                        DecorProviderKt.partitionAlignedBound(decorProviders, bound);
                decorProviders = pair.getSecond();
                createOverlay(bound, pair.getFirst(), shouldOptimizeVisibility);
            }
            for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
                if (!hasCreatedOverlay[i]) {
                    removeOverlay(i);
                }
            }

            if (shouldOptimizeVisibility) {
                mDotViewController.setShowingListener(mPrivacyDotShowingListener);
            } else {
                mDotViewController.setShowingListener(null);
            }
            final View tl, tr, bl, br;
            if ((tl = getOverlayView(R.id.privacy_dot_top_left_container)) != null
                    && (tr = getOverlayView(R.id.privacy_dot_top_right_container)) != null
                    && (bl = getOverlayView(R.id.privacy_dot_bottom_left_container)) != null
                    && (br = getOverlayView(R.id.privacy_dot_bottom_right_container)) != null) {
                // Overlays have been created, send the dots to the controller
                //TODO: need a better way to do this
                mDotViewController.initialize(tl, tr, bl, br);
            }
        } else {
            removeAllOverlays();
            removeHwcOverlay();
        }

        if (hasOverlays() || hasHwcOverlay()) {
            if (mIsRegistered) {
                return;
            }

            // Watch color inversion and invert the overlay as needed.
            if (mColorInversionSetting == null) {
                mColorInversionSetting = new UserSettingObserver(mSecureSettings, mHandler,
                        Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED,
                        mUserTracker.getUserId()) {
                    @Override
                    protected void handleValueChanged(int value, boolean observedChange) {
                        updateColorInversion(value);
                    }
                };
            }
            mColorInversionSetting.setListening(true);
            mColorInversionSetting.onChange(false);
            updateColorInversion(mColorInversionSetting.getValue());

            mUserTracker.addCallback(mUserChangedCallback, mExecutor);
            mIsRegistered = true;
        } else {
            if (mColorInversionSetting != null) {
                mColorInversionSetting.setListening(false);
            }

            mUserTracker.removeCallback(mUserChangedCallback);
            mIsRegistered = false;
        }
    }

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

    @VisibleForTesting
    boolean hasOverlays() {
        if (mOverlays == null) {
            return false;
        }

        for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
            if (mOverlays[i] != null) {
                return true;
            }
        }
        mOverlays = null;
        return false;
    }

    private void removeAllOverlays() {
        if (mOverlays == null) {
            return;
        }

        for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
            if (mOverlays[i] != null) {
                removeOverlay(i);
            }
        }
        mOverlays = null;
    }

    private void removeOverlay(@BoundsPosition int pos) {
        if (mOverlays == null || mOverlays[pos] == null) {
            return;
        }
        mWindowManager.removeViewImmediate(mOverlays[pos].getRootView());
        mOverlays[pos] = null;
    }

    @View.Visibility
    private int getWindowVisibility(@NonNull OverlayWindow overlay,
            boolean shouldOptimizeVisibility) {
        if (!shouldOptimizeVisibility) {
            // All overlays have visible views so there's no need to optimize visibility.
            // For example, the rounded corners could exist in each overlay and since the rounded
            // corners are always visible, there's no need to optimize visibility.
            return View.VISIBLE;
        }

        // Optimize if it's just the privacy dot & face scanning animation, since the privacy
        // dot and face scanning overlay aren't always visible.
        int[] ids = {
                R.id.privacy_dot_top_left_container,
                R.id.privacy_dot_top_right_container,
                R.id.privacy_dot_bottom_left_container,
                R.id.privacy_dot_bottom_right_container,
                mFaceScanningViewId
        };
        for (int id: ids) {
            final View notAlwaysVisibleViews = overlay.getView(id);
            if (notAlwaysVisibleViews != null
                    && notAlwaysVisibleViews.getVisibility() == View.VISIBLE) {
                // Overlay is VISIBLE if one the views inside this overlay is VISIBLE
                return View.VISIBLE;
            }
        }

        // Only non-visible views in this overlay, so set overlay to INVISIBLE
        return View.INVISIBLE;
    }

    private void createOverlay(
            @BoundsPosition int pos,
            @NonNull List<DecorProvider> decorProviders,
            boolean shouldOptimizeVisibility) {
        if (mOverlays == null) {
            mOverlays = new OverlayWindow[BOUNDS_POSITION_LENGTH];
        }

        if (mOverlays[pos] != null) {
            initOverlay(mOverlays[pos], decorProviders, shouldOptimizeVisibility);
            return;
        }
        mOverlays[pos] = new OverlayWindow(mContext);
        initOverlay(mOverlays[pos], decorProviders, shouldOptimizeVisibility);
        final ViewGroup overlayView = mOverlays[pos].getRootView();
        overlayView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
        overlayView.setAlpha(0);
        overlayView.setForceDarkAllowed(false);

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

        overlayView.addOnLayoutChangeListener(new OnLayoutChangeListener() {
            @Override
            public void onLayoutChange(View v, int left, int top, int right, int bottom,
                    int oldLeft, int oldTop, int oldRight, int oldBottom) {
                overlayView.removeOnLayoutChangeListener(this);
                overlayView.animate()
                        .alpha(1)
                        .setDuration(1000)
                        .start();
            }
        });

        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, mDebug);
        mScreenDecorHwcWindow.addView(mScreenDecorHwcLayer, new FrameLayout.LayoutParams(
                MATCH_PARENT, MATCH_PARENT, Gravity.TOP | Gravity.START));
        mWindowManager.addView(mScreenDecorHwcWindow, getHwcWindowLayoutParams());
        updateHwLayerRoundedCornerExistAndSize();
        updateHwLayerRoundedCornerDrawable();
        mScreenDecorHwcWindow.getViewTreeObserver().addOnPreDrawListener(
                new ValidatingPreDrawListener(mScreenDecorHwcWindow));
    }

    /**
     * Init OverlayWindow with decorProviders
     */
    private void initOverlay(
            @NonNull OverlayWindow overlay,
            @NonNull List<DecorProvider> decorProviders,
            boolean shouldOptimizeVisibility) {
        if (!overlay.hasSameProviders(decorProviders)) {
            decorProviders.forEach(provider -> {
                if (overlay.getView(provider.getViewId()) != null) {
                    return;
                }
                removeOverlayView(provider.getViewId());
                overlay.addDecorProvider(provider, mRotation, mTintColor);
            });
        }
        // Use visibility of privacy dot views & face scanning view to determine the overlay's
        // visibility if the screen decoration SW layer overlay isn't persistently showing
        // (ie: rounded corners always showing in SW layer)
        overlay.getRootView().setVisibility(getWindowVisibility(overlay, shouldOptimizeVisibility));
    }

    @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");
        if (sToolkitSetFrameRateReadOnly) {
            lp.setFrameRateBoostOnTouchEnabled(false);
            lp.setFrameRatePowerSavingsBalanced(false);
        }
        lp.gravity = Gravity.TOP | Gravity.START;
        if (!mDebug) {
            lp.setColorMode(ActivityInfo.COLOR_MODE_A8);
        }
        return lp;
    }

    public static WindowManager.LayoutParams getWindowLayoutBaseParams() {
        return getWindowLayoutBaseParams(/* excludeFromScreenshots= */ true);
    }

    /**
     * Creates the base {@link WindowManager.LayoutParams} that are used for all decoration windows.
     *
     * @param excludeFromScreenshots whether to set the {@link
     *     WindowManager.LayoutParams#PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY} flag.
     */
    public static WindowManager.LayoutParams getWindowLayoutBaseParams(
            boolean excludeFromScreenshots) {
        final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                        | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                        | WindowManager.LayoutParams.FLAG_SLIPPERY
                        | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
                        | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
                PixelFormat.TRANSLUCENT);
        lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS
                | WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;

        // FLAG_SLIPPERY can only be set by trusted overlays
        lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;

        if (!DEBUG_SCREENSHOT_ROUNDED_CORNERS && excludeFromScreenshots) {
            lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
        }

        lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
        lp.setFitInsetsTypes(0 /* types */);
        lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC;
        return lp;
    }

    private int getWidthLayoutParamByPos(@BoundsPosition int pos) {
        final int rotatedPos = getBoundPositionFromRotation(pos, mRotation);
        return rotatedPos == BOUNDS_POSITION_TOP || rotatedPos == BOUNDS_POSITION_BOTTOM
                ? MATCH_PARENT : WRAP_CONTENT;
    }

    private int getHeightLayoutParamByPos(@BoundsPosition int pos) {
        final int rotatedPos = getBoundPositionFromRotation(pos, mRotation);
        return rotatedPos == BOUNDS_POSITION_TOP || rotatedPos == BOUNDS_POSITION_BOTTOM
                ? WRAP_CONTENT : MATCH_PARENT;
    }

    private static String getWindowTitleByPos(@BoundsPosition int pos) {
        switch (pos) {
            case BOUNDS_POSITION_LEFT:
                return "ScreenDecorOverlayLeft";
            case BOUNDS_POSITION_TOP:
                return "ScreenDecorOverlay";
            case BOUNDS_POSITION_RIGHT:
                return "ScreenDecorOverlayRight";
            case BOUNDS_POSITION_BOTTOM:
                return "ScreenDecorOverlayBottom";
            default:
                throw new IllegalArgumentException("unknown bound position: " + pos);
        }
    }

    private static boolean displaySizeChanged(Point size, DisplayInfo info) {
        return size.x != info.getNaturalWidth() || size.y != info.getNaturalHeight();
    }

    private int getOverlayWindowGravity(@BoundsPosition int pos) {
        final int rotated = getBoundPositionFromRotation(pos, mRotation);
        switch (rotated) {
            case BOUNDS_POSITION_TOP:
                return Gravity.TOP;
            case BOUNDS_POSITION_BOTTOM:
                return Gravity.BOTTOM;
            case BOUNDS_POSITION_LEFT:
                return Gravity.LEFT;
            case BOUNDS_POSITION_RIGHT:
                return Gravity.RIGHT;
            default:
                throw new IllegalArgumentException("unknown bound position: " + pos);
        }
    }

    @VisibleForTesting
    static int getBoundPositionFromRotation(@BoundsPosition int pos, int rotation) {
        return (pos - rotation) < 0
                ? pos - rotation + DisplayCutout.BOUNDS_POSITION_LENGTH
                : pos - rotation;
    }

    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) {
            mCameraListener =
                    CameraAvailabilityListener.Factory.build(
                            mContext, mExecutor, mCameraProtectionLoader);
            mCameraListener.addTransitionCallback(mCameraTransitionCallback);
            mCameraListener.startListening();
        }
    }

    private final UserTracker.Callback mUserChangedCallback =
            new UserTracker.Callback() {
                @Override
                public void onUserChanged(int newUser, @NonNull Context userContext) {
                    mLogger.logUserSwitched(newUser);
                    // update color inversion setting to the new user
                    mColorInversionSetting.setUserId(newUser);
                    updateColorInversion(mColorInversionSetting.getValue());
                }
            };

    /**
     * Use the current value of {@link ScreenDecorations#mColorInversionSetting} and passes it
     * to {@link ScreenDecorations#updateColorInversion}
     */
    private void updateColorInversionDefault() {
        int inversion = 0;
        if (mColorInversionSetting != null) {
            inversion = mColorInversionSetting.getValue();
        }

        updateColorInversion(inversion);
    }

    /**
     * Update the tint color of screen decoration assets. Defaults to Color.BLACK. In the case of
     * a color inversion being set, use Color.WHITE (which inverts to black).
     *
     * When {@link ScreenDecorations#mDebug} is {@code true}, this value is updated to use
     * {@link ScreenDecorations#mDebugColor}, and does not handle inversion.
     *
     * @param colorsInvertedValue if non-zero, assume that colors are inverted, and use Color.WHITE
     *                            for screen decoration tint
     */
    private void updateColorInversion(int colorsInvertedValue) {
        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[] {
                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,
                R.id.rounded_corner_bottom_right
        });
    }

    @VisibleForTesting
    float getPhysicalPixelDisplaySizeRatio() {
        mContext.getDisplay().getDisplayInfo(mDisplayInfo);
        final Display.Mode maxDisplayMode =
                DisplayUtils.getMaximumResolutionDisplayMode(mDisplayInfo.supportedModes);
        if (maxDisplayMode == null) {
            return 1f;
        }
        return DisplayUtils.getPhysicalPixelDisplaySizeRatio(
                maxDisplayMode.getPhysicalWidth(), maxDisplayMode.getPhysicalHeight(),
                mDisplayInfo.getNaturalWidth(), mDisplayInfo.getNaturalHeight());
    }

    @Override
    public void onConfigChanged(Configuration newConfig) {
        if (DEBUG_DISABLE_SCREEN_DECORATIONS) {
            Log.i(TAG, "ScreenDecorations is disabled");
            return;
        }

        mExecutor.execute(() -> {
            Trace.beginSection("ScreenDecorations#onConfigurationChanged");
            mContext.getDisplay().getDisplayInfo(mDisplayInfo);
            if (displaySizeChanged(mDisplaySize, mDisplayInfo)) {
                // We expect the display change event to happen first, but in this case, we received
                // onConfigurationChanged first.
                // Return so that we handle the display change event first, and then manually
                // trigger the config update.
                mPendingManualConfigUpdate = true;
                return;
            }
            int oldRotation = mRotation;
            mPendingConfigChange = false;
            updateConfiguration();
            if (oldRotation != mRotation) {
                mLogger.logRotationChanged(oldRotation, mRotation);
            }
            setupDecorations();
            if (mOverlays != null) {
                // Updating the layout params ensures that ViewRootImpl will call relayoutWindow(),
                // which ensures that the forced seamless rotation will end, even if we updated
                // the rotation before window manager was ready (and was still waiting for sending
                // the updated rotation).
                updateLayoutParams();
            }
            Trace.endSection();
        });
    }

    private static String alphaInterpretationToString(int alpha) {
        switch (alpha) {
            case AlphaInterpretation.COVERAGE: return "COVERAGE";
            case AlphaInterpretation.MASK:     return "MASK";
            default:                           return "Unknown: " + alpha;
        }
    }

    @NeverCompile
    @Override
    public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
        pw.println("ScreenDecorations state:");
        IndentingPrintWriter ipw = asIndenting(pw);
        ipw.increaseIndent();
        ipw.println("DEBUG_DISABLE_SCREEN_DECORATIONS:" + DEBUG_DISABLE_SCREEN_DECORATIONS);
        if (DEBUG_DISABLE_SCREEN_DECORATIONS) {
            return;
        }
        ipw.println("mDebug:" + mDebug);

        ipw.println("mIsPrivacyDotEnabled:" + isPrivacyDotEnabled());
        ipw.println("shouldOptimizeOverlayVisibility:" + shouldOptimizeVisibility());
        final boolean supportsShowingFaceScanningAnim = mFaceScanningFactory.getHasProviders();
        ipw.println("supportsShowingFaceScanningAnim:" + supportsShowingFaceScanningAnim);
        if (supportsShowingFaceScanningAnim) {
            ipw.increaseIndent();
            ipw.println("canShowFaceScanningAnim:"
                    + mFaceScanningFactory.canShowFaceScanningAnim());
            ipw.println("shouldShowFaceScanningAnim (at time dump was taken):"
                    + mFaceScanningFactory.shouldShowFaceScanningAnim());
            ipw.decreaseIndent();
        }
        FaceScanningOverlay faceScanningOverlay =
                (FaceScanningOverlay) getOverlayView(mFaceScanningViewId);
        if (faceScanningOverlay != null) {
            faceScanningOverlay.dump(ipw);
        }
        ipw.println("mPendingConfigChange:" + mPendingConfigChange);
        if (mHwcScreenDecorationSupport != null) {
            ipw.increaseIndent();
            ipw.println("mHwcScreenDecorationSupport:");
            ipw.increaseIndent();
            ipw.println("format="
                    + PixelFormat.formatToString(mHwcScreenDecorationSupport.format));
            ipw.println("alphaInterpretation="
                    + alphaInterpretationToString(mHwcScreenDecorationSupport.alphaInterpretation));
            ipw.decreaseIndent();
            ipw.decreaseIndent();
        } else {
            ipw.increaseIndent();
            pw.println("mHwcScreenDecorationSupport: null");
            ipw.decreaseIndent();
        }
        if (mScreenDecorHwcLayer != null) {
            ipw.increaseIndent();
            mScreenDecorHwcLayer.dump(ipw);
            ipw.decreaseIndent();
        } else {
            ipw.println("mScreenDecorHwcLayer: null");
        }
        if (mOverlays != null) {
            ipw.println("mOverlays(left,top,right,bottom)=("
                    + (mOverlays[BOUNDS_POSITION_LEFT] != null) + ","
                    + (mOverlays[BOUNDS_POSITION_TOP] != null) + ","
                    + (mOverlays[BOUNDS_POSITION_RIGHT] != null) + ","
                    + (mOverlays[BOUNDS_POSITION_BOTTOM] != null) + ")");

            for (int i = BOUNDS_POSITION_LEFT; i < BOUNDS_POSITION_LENGTH; i++) {
                if (mOverlays[i] != null) {
                    mOverlays[i].dump(pw, getWindowTitleByPos(i));
                }
            }
        }
        mRoundedCornerResDelegate.dump(pw, args);
        mDebugRoundedCornerDelegate.dump(pw);
    }

    @VisibleForTesting
    void updateConfiguration() {
        Preconditions.checkState(mHandler.getLooper().getThread() == Thread.currentThread(),
                "must call on " + mHandler.getLooper().getThread()
                        + ", but was " + Thread.currentThread());

        mContext.getDisplay().getDisplayInfo(mDisplayInfo);
        final int newRotation = mDisplayInfo.rotation;
        if (mRotation != newRotation) {
            mDotViewController.setNewRotation(newRotation);
        }
        final DisplayCutout newCutout = mDisplayInfo.displayCutout;

        if (!mPendingConfigChange
                && (newRotation != mRotation || displaySizeChanged(mDisplaySize, mDisplayInfo)
                || !Objects.equals(newCutout, mDisplayCutout))) {
            mRotation = newRotation;
            mDisplaySize.x = mDisplayInfo.getNaturalWidth();
            mDisplaySize.y = mDisplayInfo.getNaturalHeight();
            mDisplayCutout = newCutout;
            float ratio = getPhysicalPixelDisplaySizeRatio();
            mRoundedCornerResDelegate.setPhysicalPixelDisplaySizeRatio(ratio);
            mDebugRoundedCornerDelegate.setPhysicalPixelDisplaySizeRatio(ratio);
            if (mScreenDecorHwcLayer != null) {
                mScreenDecorHwcLayer.pendingConfigChange = false;
                mScreenDecorHwcLayer.updateConfiguration(mDisplayUniqueId);
                updateHwLayerRoundedCornerExistAndSize();
                updateHwLayerRoundedCornerDrawable();
            }
            updateLayoutParams();

            // update all provider views inside overlay
            updateOverlayProviderViews(null);
        }

        FaceScanningOverlay faceScanningOverlay =
                (FaceScanningOverlay) getOverlayView(mFaceScanningViewId);
        if (faceScanningOverlay != null) {
            faceScanningOverlay.updateColors();
        }
    }

    private boolean hasRoundedCorners() {
        return mRoundedCornerFactory.getHasProviders()
                || mDebugRoundedCornerFactory.getHasProviders();
    }

    private boolean shouldOptimizeVisibility() {
        return (isPrivacyDotEnabled() || mFaceScanningFactory.getHasProviders())
                && (mHwcScreenDecorationSupport != null
                    || (!hasRoundedCorners() && !shouldDrawCutout())
                );
    }

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

    static boolean shouldDrawCutout(Context context) {
        return DisplayCutout.getFillBuiltInDisplayCutout(
                context.getResources(), context.getDisplay().getUniqueId());
    }

    @VisibleForTesting
    void updateOverlayProviderViews(@Nullable Integer[] filterIds) {
        if (mOverlays == null) {
            return;
        }
        if (mPendingConfigChange) {
            // Let RestartingPreDrawListener's onPreDraw call updateConfiguration
            // -> updateOverlayProviderViews to redraw with display change synchronously.
            return;
        }
        ++mProviderRefreshToken;
        for (final OverlayWindow overlay: mOverlays) {
            if (overlay == null) {
                continue;
            }
            overlay.onReloadResAndMeasure(filterIds, mProviderRefreshToken, mRotation, mTintColor,
                    mDisplayUniqueId);
        }
    }

    private void updateLayoutParams() {
        //ToDo: We should skip unnecessary call to update view layout.
        Trace.beginSection("ScreenDecorations#updateLayoutParams");
        if (mScreenDecorHwcWindow != null) {
            mWindowManager.updateViewLayout(mScreenDecorHwcWindow, getHwcWindowLayoutParams());
        }

        if (mOverlays != null) {
            for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
                if (mOverlays[i] == null) {
                    continue;
                }
                mWindowManager.updateViewLayout(
                        mOverlays[i].getRootView(), getWindowLayoutParams(i));
            }
        }
        Trace.endSection();
    }

    private void updateHwLayerRoundedCornerDrawable() {
        if (mScreenDecorHwcLayer == null) {
            return;
        }

        Drawable topDrawable = mRoundedCornerResDelegate.getTopRoundedDrawable();
        Drawable bottomDrawable = mRoundedCornerResDelegate.getBottomRoundedDrawable();
        if (mDebug && (mDebugRoundedCornerFactory.getHasProviders())) {
            topDrawable = mDebugRoundedCornerDelegate.getTopRoundedDrawable();
            bottomDrawable = mDebugRoundedCornerDelegate.getBottomRoundedDrawable();
        }

        if (topDrawable == null && bottomDrawable == null) {
            return;
        }
        mScreenDecorHwcLayer.updateRoundedCornerDrawable(topDrawable, bottomDrawable);
    }

    private void updateHwLayerRoundedCornerExistAndSize() {
        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) {
        LayoutParams params = view.getLayoutParams();
        params.width = pixelSize.getWidth();
        params.height = pixelSize.getHeight();
        view.setLayoutParams(params);
    }

    @VisibleForTesting
    void onFaceSensorLocationChanged(Point location) {
        mLogger.onSensorLocationChanged();

        if (mExecutor != null) {
            mExecutor.execute(
                    () -> {
                        if (getOverlayView(mFaceScanningViewId) == null) {
                            // face sensor location was just initialized
                            setupDecorations();
                        } else {
                            updateOverlayProviderViews(new Integer[]{mFaceScanningViewId});
                        }
                    }
            );
        }
    }

    public static class DisplayCutoutView extends DisplayCutoutBaseView {
        final List<Rect> mBounds = new ArrayList();
        final Rect mBoundingRect = new Rect();
        Rect mTotalBounds = new Rect();
        boolean mDebug = false;

        private int mColor = Color.BLACK;
        private int mRotation;
        private int mInitialPosition;
        private int mPosition;

        public DisplayCutoutView(Context context, @BoundsPosition int pos) {
            super(context);
            mInitialPosition = pos;

            paint.setColor(mColor);
            paint.setStyle(Paint.Style.FILL);
            if (DEBUG_LOGGING) {
                getViewTreeObserver().addOnDrawListener(() -> Log.i(TAG,
                        getWindowTitleByPos(pos) + " drawn in rot " + mRotation));
            }
        }

        public void setDebug(boolean debug) {
            mDebug = debug;
        }

        public void setColor(int color) {
            if (color == mColor) {
                return;
            }
            mColor = color;
            paint.setColor(mColor);
            invalidate();
        }

        @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);
        }

        @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
        @Override
        public void updateCutout() {
            if (!isAttachedToWindow() || pendingConfigChange) {
                return;
            }
            mPosition = getBoundPositionFromRotation(mInitialPosition, mRotation);
            requestLayout();
            getDisplay().getDisplayInfo(displayInfo);
            mBounds.clear();
            mBoundingRect.setEmpty();
            cutoutPath.reset();
            int newVisible;
            if (shouldDrawCutout(getContext()) && hasCutout()) {
                mBounds.addAll(displayInfo.displayCutout.getBoundingRects());
                localBounds(mBoundingRect);
                updateGravity();
                updateBoundingPath();
                invalidate();
                newVisible = VISIBLE;
            } else {
                newVisible = GONE;
            }
            if (updateVisOnUpdateCutout() && newVisible != getVisibility()) {
                setVisibility(newVisible);
            }
        }

        protected boolean updateVisOnUpdateCutout() {
            return true;
        }

        private void updateBoundingPath() {
            final Path path = displayInfo.displayCutout.getCutoutPath();
            if (path != null) {
                cutoutPath.set(path);
            } else {
                cutoutPath.reset();
            }
        }

        private void updateGravity() {
            LayoutParams lp = getLayoutParams();
            if (lp instanceof FrameLayout.LayoutParams) {
                FrameLayout.LayoutParams flp = (FrameLayout.LayoutParams) lp;
                int newGravity = getGravity(displayInfo.displayCutout);
                if (flp.gravity != newGravity) {
                    flp.gravity = newGravity;
                    setLayoutParams(flp);
                }
            }
        }

        private boolean hasCutout() {
            final DisplayCutout displayCutout = displayInfo.displayCutout;
            if (displayCutout == null) {
                return false;
            }

            if (mPosition == BOUNDS_POSITION_LEFT) {
                return !displayCutout.getBoundingRectLeft().isEmpty();
            } else if (mPosition == BOUNDS_POSITION_TOP) {
                return !displayCutout.getBoundingRectTop().isEmpty();
            } else if (mPosition == BOUNDS_POSITION_BOTTOM) {
                return !displayCutout.getBoundingRectBottom().isEmpty();
            } else if (mPosition == BOUNDS_POSITION_RIGHT) {
                return !displayCutout.getBoundingRectRight().isEmpty();
            }
            return false;
        }

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            if (mBounds.isEmpty()) {
                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
                return;
            }

            if (showProtection) {
                // Make sure that our measured height encompasses the protection
                mTotalBounds.set(mBoundingRect);
                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));
            } else {
                setMeasuredDimension(
                        resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0),
                        resolveSizeAndState(mBoundingRect.height(), heightMeasureSpec, 0));
            }
        }

        public static void boundsFromDirection(DisplayCutout displayCutout, int gravity,
                Rect out) {
            switch (gravity) {
                case Gravity.TOP:
                    out.set(displayCutout.getBoundingRectTop());
                    break;
                case Gravity.LEFT:
                    out.set(displayCutout.getBoundingRectLeft());
                    break;
                case Gravity.BOTTOM:
                    out.set(displayCutout.getBoundingRectBottom());
                    break;
                case Gravity.RIGHT:
                    out.set(displayCutout.getBoundingRectRight());
                    break;
                default:
                    out.setEmpty();
            }
        }

        private void localBounds(Rect out) {
            DisplayCutout displayCutout = displayInfo.displayCutout;
            boundsFromDirection(displayCutout, getGravity(displayCutout), out);
        }

        private int getGravity(DisplayCutout displayCutout) {
            if (mPosition == BOUNDS_POSITION_LEFT) {
                if (!displayCutout.getBoundingRectLeft().isEmpty()) {
                    return Gravity.LEFT;
                }
            } else if (mPosition == BOUNDS_POSITION_TOP) {
                if (!displayCutout.getBoundingRectTop().isEmpty()) {
                    return Gravity.TOP;
                }
            } else if (mPosition == BOUNDS_POSITION_BOTTOM) {
                if (!displayCutout.getBoundingRectBottom().isEmpty()) {
                    return Gravity.BOTTOM;
                }
            } else if (mPosition == BOUNDS_POSITION_RIGHT) {
                if (!displayCutout.getBoundingRectRight().isEmpty()) {
                    return Gravity.RIGHT;
                }
            }
            return Gravity.NO_GRAVITY;
        }
    }

    /**
     * A pre-draw listener, that cancels the draw and restarts the traversal with the updated
     * window attributes.
     */
    private class RestartingPreDrawListener implements ViewTreeObserver.OnPreDrawListener {

        private final View mView;
        private final int mTargetRotation;
        private final Point mTargetDisplaySize;
        // 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,
                int targetRotation, Point targetDisplaySize) {
            mView = view;
            mTargetRotation = targetRotation;
            mTargetDisplaySize = targetDisplaySize;
            mPosition = position;
        }

        @Override
        public boolean onPreDraw() {
            mView.getViewTreeObserver().removeOnPreDrawListener(this);
            if (mTargetRotation == mRotation && mDisplaySize.equals(mTargetDisplaySize)) {
                if (DEBUG_LOGGING) {
                    final String title = mPosition < 0 ? "ScreenDecorHwcLayer"
                            : getWindowTitleByPos(mPosition);
                    Log.i(TAG, title + " already in target rot "
                            + mTargetRotation + " and in target resolution "
                            + mTargetDisplaySize.x + "x" + mTargetDisplaySize.y
                            + ", allow draw without restarting it");
                }
                return true;
            }

            mPendingConfigChange = false;
            // This changes the window attributes - we need to restart the traversal for them to
            // take effect.
            updateConfiguration();
            if (DEBUG_LOGGING) {
                final String title = mPosition < 0 ? "ScreenDecorHwcLayer"
                        : getWindowTitleByPos(mPosition);
                Log.i(TAG, title
                        + " restarting listener fired, restarting draw for rot " + mRotation
                        + ", resolution " + mDisplaySize.x + "x" + mDisplaySize.y);
            }
            mView.invalidate();
            return false;
        }
    }

    /**
     * A pre-draw listener, that validates that the rotation and display resolution we draw in
     * matches the display's rotation and resolution before continuing the draw.
     *
     * This is to prevent a race condition, where we have not received the display changed event
     * yet, and would thus draw in an old orientation.
     */
    private class ValidatingPreDrawListener implements ViewTreeObserver.OnPreDrawListener {

        private final View mView;

        public ValidatingPreDrawListener(View view) {
            mView = view;
        }

        @Override
        public boolean onPreDraw() {
            mContext.getDisplay().getDisplayInfo(mDisplayInfo);
            final int displayRotation = mDisplayInfo.rotation;
            if ((displayRotation != mRotation || displaySizeChanged(mDisplaySize, mDisplayInfo))
                    && !mPendingConfigChange) {
                if (DEBUG_LOGGING) {
                    if (displayRotation != mRotation) {
                        Log.i(TAG, "Drawing rot " + mRotation + ", but display is at rot "
                                + displayRotation + ". Restarting draw");
                    }
                    if (displaySizeChanged(mDisplaySize, mDisplayInfo)) {
                        Log.i(TAG, "Drawing at " + mDisplaySize.x + "x" + mDisplaySize.y
                                + ", but display is at "
                                + mDisplayInfo.getNaturalWidth() + "x"
                                + mDisplayInfo.getNaturalHeight() + ". Restarting draw");
                    }
                }
                mView.invalidate();
                return false;
            }
            return true;
        }
    }
}
