/*
 * 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.settingslib.Utils;
import com.android.systemui.biometrics.AuthController;
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.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.statusbar.commandline.CommandRegistry;
import com.android.systemui.statusbar.events.PrivacyDotViewController;
import com.android.systemui.util.concurrency.DelayableExecutor;
import com.android.systemui.util.concurrency.ThreadFactory;
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, Dumpable {
    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 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 final AuthController mAuthController;

    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 ThreadFactory mThreadFactory;
    private final DecorProviderFactory mDotFactory;
    private final FaceScanningProviderFactory mFaceScanningFactory;
    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;
    @Nullable
    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;

    @VisibleForTesting
    protected void showCameraProtection(@NonNull Path protectionPath, @NonNull Rect bounds) {
        if (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,
            ThreadFactory threadFactory,
            PrivacyDotDecorProviderFactory dotFactory,
            FaceScanningProviderFactory faceScanningFactory,
            ScreenDecorationsLogger logger,
            AuthController authController) {
        mContext = context;
        mSecureSettings = secureSettings;
        mCommandRegistry = commandRegistry;
        mUserTracker = userTracker;
        mDisplayTracker = displayTracker;
        mDotViewController = dotViewController;
        mThreadFactory = threadFactory;
        mDotFactory = dotFactory;
        mFaceScanningFactory = faceScanningFactory;
        mFaceScanningViewId = com.android.systemui.res.R.id.face_scanning_anim;
        mLogger = logger;
        mAuthController = authController;
    }


    private final AuthController.Callback mAuthControllerCallback = new AuthController.Callback() {
        @Override
        public void onFaceSensorLocationChanged() {
            mLogger.onSensorLocationChanged();
            if (mExecutor != null) {
                mExecutor.execute(
                        () -> updateOverlayProviderViews(
                                new Integer[]{mFaceScanningViewId}));
            }
        }
    };

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

    @Override
    public void start() {
        if (DEBUG_DISABLE_SCREEN_DECORATIONS) {
            Log.i(TAG, "ScreenDecorations is disabled");
            return;
        }
        mHandler = mThreadFactory.buildHandlerOnNewThread("ScreenDecorations");
        mExecutor = mThreadFactory.buildDelayableExecutorOnHandler(mHandler);
        mExecutor.execute(this::startOnScreenDecorationsThread);
        mDotViewController.setUiExecutor(mExecutor);
        mAuthController.addCallback(mAuthControllerCallback);
        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");
        mWindowManager = mContext.getSystemService(WindowManager.class);
        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;
                    }
                }
            }
        };
        mDisplayTracker.addDisplayChangeCallback(mDisplayListener, new HandlerExecutor(mHandler));
        updateConfiguration();
        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 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");
        lp.gravity = Gravity.TOP | Gravity.START;
        if (!mDebug) {
            lp.setColorMode(ActivityInfo.COLOR_MODE_A8);
        }
        return lp;
    }

    private WindowManager.LayoutParams getWindowLayoutBaseParams() {
        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_SPLIT_TOUCH
                        | 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) {
            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);
            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 onConfigurationChanged(Configuration newConfig) {
        if (DEBUG_DISABLE_SCREEN_DECORATIONS) {
            Log.i(TAG, "ScreenDecorations is disabled");
            return;
        }

        mExecutor.execute(() -> {
            Trace.beginSection("ScreenDecorations#onConfigurationChanged");
            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.setFaceScanningAnimColor(
                    Utils.getColorAttrDefaultColor(faceScanningOverlay.getContext(),
                            com.android.systemui.res.R.attr.wallpaperTextColorAccent));
        }
    }

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

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

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