/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.launcher3.util.window;

import static android.view.Display.DEFAULT_DISPLAY;

import static com.android.launcher3.Utilities.dpToPx;
import static com.android.launcher3.Utilities.dpiFromPx;
import static com.android.launcher3.testing.shared.ResourceUtils.INVALID_RESOURCE_HANDLE;
import static com.android.launcher3.testing.shared.ResourceUtils.NAVBAR_HEIGHT;
import static com.android.launcher3.testing.shared.ResourceUtils.NAVBAR_HEIGHT_LANDSCAPE;
import static com.android.launcher3.testing.shared.ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE;
import static com.android.launcher3.testing.shared.ResourceUtils.NAV_BAR_INTERACTION_MODE_RES_NAME;
import static com.android.launcher3.testing.shared.ResourceUtils.STATUS_BAR_HEIGHT;
import static com.android.launcher3.testing.shared.ResourceUtils.STATUS_BAR_HEIGHT_LANDSCAPE;
import static com.android.launcher3.testing.shared.ResourceUtils.STATUS_BAR_HEIGHT_PORTRAIT;
import static com.android.launcher3.util.RotationUtils.deltaRotation;
import static com.android.launcher3.util.RotationUtils.rotateRect;
import static com.android.launcher3.util.RotationUtils.rotateSize;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Insets;
import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.os.Build;
import android.util.ArrayMap;
import android.util.Log;
import android.view.Display;
import android.view.DisplayCutout;
import android.view.Surface;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.core.view.DisplayCutoutCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.window.layout.WindowMetricsCalculator;

import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.dagger.LauncherAppSingleton;
import com.android.launcher3.dagger.LauncherBaseAppComponent;
import com.android.launcher3.testing.shared.ResourceUtils;
import com.android.launcher3.util.DaggerSingletonObject;
import com.android.launcher3.util.NavigationMode;
import com.android.launcher3.util.WindowBounds;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.inject.Inject;

/**
 * Utility class for mocking some window manager behaviours
 */
@LauncherAppSingleton
public class WindowManagerProxy {

    private static final String TAG = "WindowManagerProxy";
    public static final int MIN_TABLET_WIDTH = 600;

    public static final DaggerSingletonObject<WindowManagerProxy> INSTANCE =
            new DaggerSingletonObject<>(LauncherBaseAppComponent::getWmProxy);

    protected final boolean mTaskbarDrawnInProcess;

    @Inject
    public WindowManagerProxy() {
        this(false);
    }

    protected WindowManagerProxy(boolean taskbarDrawnInProcess) {
        mTaskbarDrawnInProcess = taskbarDrawnInProcess;
    }

    /**
     * Returns true if taskbar is drawn in process
     */
    public boolean isTaskbarDrawnInProcess() {
        return mTaskbarDrawnInProcess;
    }

    /**
     * Returns a map of normalized info of internal displays to estimated window bounds
     * for that display
     */
    public ArrayMap<CachedDisplayInfo, List<WindowBounds>> estimateInternalDisplayBounds(
            Context displayInfoContext) {
        CachedDisplayInfo info = getDisplayInfo(displayInfoContext).normalize(this);
        List<WindowBounds> bounds = estimateWindowBounds(displayInfoContext, info);
        ArrayMap<CachedDisplayInfo, List<WindowBounds>> result = new ArrayMap<>();
        result.put(info, bounds);
        return result;
    }

    /**
     * Returns if we are in desktop mode or not.
     */
    public boolean isInDesktopMode(int displayId) {
        return false;
    }

    /**
     * Returns if the pinned taskbar should be shown when home is visible.
     */
    public boolean showLockedTaskbarOnHome(Context displayInfoContext) {
        return false;
    }

    /**
     * Returns whether the display is a freeform display for which taskbar should be pinned
     * and showing desktop tasks.
     */
    public boolean showDesktopTaskbarForFreeformDisplay(Context displayInfoContext) {
        return false;
    }

    /**
     * Returns if the home is visible.
     */
    public boolean isHomeVisible(Context context) {
        return false;
    }

    /**
     * Returns the real bounds for the provided display after applying any insets normalization
     */
    @TargetApi(Build.VERSION_CODES.R)
    public WindowBounds getRealBounds(Context displayInfoContext, CachedDisplayInfo info) {
        if (!Utilities.ATLEAST_R) {
            var current = WindowMetricsCalculator.getOrCreate()
                    .computeMaximumWindowMetrics(displayInfoContext.getApplicationContext());
            Display display = getDisplay(displayInfoContext);
            List<WindowBounds> windowBounds = estimateInternalDisplayBounds(displayInfoContext).get(
                    getDisplayInfo(displayInfoContext).normalize(this));
            var estimated = windowBounds.get(display.getRotation());
            return new WindowBounds(current.getBounds(), estimated.insets, info.rotation);
        }

        WindowMetrics wm = displayInfoContext.getSystemService(WindowManager.class)
                    .getMaximumWindowMetrics();
        Rect insets = new Rect();
        normalizeWindowInsets(displayInfoContext, wm.getWindowInsets(), insets);
        return new WindowBounds(wm.getBounds(), insets, info.rotation);
    }

    /**
     * Returns an updated insets, accounting for various Launcher UI specific overrides like taskbar
     */
    public WindowInsets normalizeWindowInsets(Context context, WindowInsets oldInsets,
            Rect outInsets) {
        if (!Utilities.ATLEAST_R || !mTaskbarDrawnInProcess) {
            outInsets.set(oldInsets.getSystemWindowInsetLeft(), oldInsets.getSystemWindowInsetTop(),
                    oldInsets.getSystemWindowInsetRight(), oldInsets.getSystemWindowInsetBottom());
            return oldInsets;
        }

        WindowInsets.Builder insetsBuilder = new WindowInsets.Builder(oldInsets);
        Insets navInsets = oldInsets.getInsets(WindowInsets.Type.navigationBars());

        Resources systemRes = context.getResources();
        Configuration config = systemRes.getConfiguration();

        boolean isLargeScreen = config.smallestScreenWidthDp > MIN_TABLET_WIDTH;
        boolean isGesture = isGestureNav(context);
        boolean isPortrait = config.screenHeightDp > config.screenWidthDp;

        int bottomNav = isLargeScreen
                ? 0
                : (isPortrait
                        ? getDimenByName(systemRes, NAVBAR_HEIGHT)
                        : (isGesture
                                ? getDimenByName(systemRes, NAVBAR_HEIGHT_LANDSCAPE)
                                : 0));
        int leftNav = navInsets.left;
        int rightNav = navInsets.right;
        if (!isLargeScreen && !isGesture && !isPortrait) {
            // In 3-button landscape/seascape, Launcher should always have nav insets regardless if
            // it's initiated from fullscreen apps.
            int navBarWidth = getDimenByName(systemRes, NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE);
            switch (getRotation(context)) {
                case Surface.ROTATION_90 -> rightNav = navBarWidth;
                case Surface.ROTATION_270 -> leftNav = navBarWidth;
            }
        }
        Insets newNavInsets = Insets.of(leftNav, navInsets.top, rightNav, bottomNav);
        insetsBuilder.setInsets(WindowInsets.Type.navigationBars(), newNavInsets);
        insetsBuilder.setInsetsIgnoringVisibility(WindowInsets.Type.navigationBars(), newNavInsets);

        Insets statusBarInsets = oldInsets.getInsets(WindowInsets.Type.statusBars());

        Insets newStatusBarInsets = Insets.of(
                statusBarInsets.left,
                getStatusBarHeight(context, isPortrait, statusBarInsets.top),
                statusBarInsets.right,
                statusBarInsets.bottom);
        insetsBuilder.setInsets(WindowInsets.Type.statusBars(), newStatusBarInsets);
        insetsBuilder.setInsetsIgnoringVisibility(
                WindowInsets.Type.statusBars(), newStatusBarInsets);

        // Override the tappable insets to be 0 on the bottom for gesture nav (otherwise taskbar
        // would count towards it). This is used for the bottom protection in All Apps for example.
        if (isGesture) {
            Insets oldTappableInsets = oldInsets.getInsets(WindowInsets.Type.tappableElement());
            Insets newTappableInsets = Insets.of(oldTappableInsets.left, oldTappableInsets.top,
                    oldTappableInsets.right, 0);
            insetsBuilder.setInsets(WindowInsets.Type.tappableElement(), newTappableInsets);
        }

        applyDisplayCutoutBottomInsetOverrideOnLargeScreen(
                context, isLargeScreen, dpToPx(config.screenWidthDp), oldInsets, insetsBuilder);

        WindowInsets result = insetsBuilder.build();
        Insets systemWindowInsets = result.getInsetsIgnoringVisibility(
                WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
        outInsets.set(systemWindowInsets.left, systemWindowInsets.top, systemWindowInsets.right,
                systemWindowInsets.bottom);
        return result;
    }

    /**
     * For large screen, when display cutout is at bottom left/right corner of screen, override
     * display cutout's bottom inset to 0, because launcher allows drawing content over that area.
     */
    private static void applyDisplayCutoutBottomInsetOverrideOnLargeScreen(
            @NonNull Context context,
            boolean isLargeScreen,
            int screenWidthPx,
            @NonNull WindowInsets windowInsets,
            @NonNull WindowInsets.Builder insetsBuilder) {
        if (!isLargeScreen) {
            return;
        }

        final DisplayCutout displayCutout = windowInsets.getDisplayCutout();
        if (displayCutout == null) {
            return;
        }

        if (!areBottomDisplayCutoutsSmallAndAtCorners(
                displayCutout.getBoundingRectBottom(), screenWidthPx, context.getResources())) {
            return;
        }

        Insets oldDisplayCutoutInset = windowInsets.getInsets(WindowInsets.Type.displayCutout());
        Insets newDisplayCutoutInset = Insets.of(
                oldDisplayCutoutInset.left,
                oldDisplayCutoutInset.top,
                oldDisplayCutoutInset.right,
                0);
        insetsBuilder.setInsetsIgnoringVisibility(
                WindowInsets.Type.displayCutout(), newDisplayCutoutInset);
    }

    /**
     * @see doc at {@link #areBottomDisplayCutoutsSmallAndAtCorners(Rect, int, int)}
     */
    private static boolean areBottomDisplayCutoutsSmallAndAtCorners(
            @NonNull Rect cutoutRectBottom, int screenWidthPx, @NonNull Resources res) {
        return areBottomDisplayCutoutsSmallAndAtCorners(cutoutRectBottom, screenWidthPx,
                res.getDimensionPixelSize(R.dimen.max_width_and_height_of_small_display_cutout));
    }

    /**
     * Return true if bottom display cutouts are at bottom left/right corners, AND has width or
     * height <= maxWidthAndHeightOfSmallCutoutPx. Note that display cutout rect and screenWidthPx
     * passed to this method should be in the SAME screen rotation.
     *
     * @param cutoutRectBottom bottom display cutout rect, this is based on current screen rotation
     * @param screenWidthPx screen width in px based on current screen rotation
     * @param maxWidthAndHeightOfSmallCutoutPx maximum width and height pixels of cutout.
     */
    @VisibleForTesting
    static boolean areBottomDisplayCutoutsSmallAndAtCorners(
            @NonNull Rect cutoutRectBottom, int screenWidthPx,
            int maxWidthAndHeightOfSmallCutoutPx) {
        // Empty cutoutRectBottom means there is no display cutout at the bottom. We should ignore
        // it by returning false.
        if (cutoutRectBottom.isEmpty()) {
            return false;
        }
        return (cutoutRectBottom.right <= maxWidthAndHeightOfSmallCutoutPx)
                || cutoutRectBottom.left >= (screenWidthPx - maxWidthAndHeightOfSmallCutoutPx);
    }

    protected int getStatusBarHeight(Context context, boolean isPortrait, int statusBarInset) {
        Resources systemRes = context.getResources();
        int statusBarHeight = getDimenByName(systemRes,
                isPortrait ? STATUS_BAR_HEIGHT_PORTRAIT : STATUS_BAR_HEIGHT_LANDSCAPE,
                STATUS_BAR_HEIGHT);

        return Math.max(statusBarInset, statusBarHeight);
    }

    /**
     * Returns a list of possible WindowBounds for the display keyed on the 4 surface rotations
     */
    protected List<WindowBounds> estimateWindowBounds(Context context,
            final CachedDisplayInfo displayInfo) {
        int densityDpi = context.getResources().getConfiguration().densityDpi;
        final int rotation = displayInfo.rotation;

        int minSize = Math.min(displayInfo.size.x, displayInfo.size.y);
        int swDp = (int) dpiFromPx(minSize, densityDpi);

        Resources systemRes;
        {
            Configuration conf = new Configuration();
            conf.smallestScreenWidthDp = swDp;
            systemRes = context.createConfigurationContext(conf).getResources();
        }

        boolean isTablet = swDp >= MIN_TABLET_WIDTH;
        boolean isGesture = isGestureNav(context);
        boolean isTabletOrGesture = isTablet || isGesture;

        // Use the status bar height resources because current system API to get the status bar
        // height doesn't allow to do this for an arbitrary display, it returns value only
        // for the current active display (see com.android.internal.policy.StatusBarUtils)
        int statusBarHeightPortrait = getDimenByName(systemRes,
                STATUS_BAR_HEIGHT_PORTRAIT, STATUS_BAR_HEIGHT);
        int statusBarHeightLandscape = getDimenByName(systemRes,
                STATUS_BAR_HEIGHT_LANDSCAPE, STATUS_BAR_HEIGHT);

        int navBarHeightPortrait, navBarHeightLandscape, navbarWidthLandscape;

        navBarHeightPortrait = isTablet
                ? (mTaskbarDrawnInProcess
                ? 0 : context.getResources().getDimensionPixelSize(R.dimen.taskbar_size))
                : (isGesture ? 0 : getDimenByName(systemRes, NAVBAR_HEIGHT));

        navBarHeightLandscape = isTablet
                ? (mTaskbarDrawnInProcess
                ? 0 : context.getResources().getDimensionPixelSize(R.dimen.taskbar_size))
                : (isTabletOrGesture
                ? getDimenByName(systemRes, NAVBAR_HEIGHT_LANDSCAPE) : 0);
        navbarWidthLandscape = isTabletOrGesture
                ? 0
                : getDimenByName(systemRes, NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE);

        List<WindowBounds> result = new ArrayList<>(4);
        Point tempSize = new Point();
        for (int i = 0; i < 4; i++) {
            int rotationChange = deltaRotation(rotation, i);
            tempSize.set(displayInfo.size.x, displayInfo.size.y);
            rotateSize(tempSize, rotationChange);
            Rect bounds = new Rect(0, 0, tempSize.x, tempSize.y);

            int navBarHeight, navbarWidth, statusBarHeight;
            if (tempSize.y > tempSize.x) {
                navBarHeight = navBarHeightPortrait;
                navbarWidth = 0;
                statusBarHeight = statusBarHeightPortrait;
            } else {
                navBarHeight = navBarHeightLandscape;
                navbarWidth = navbarWidthLandscape;
                statusBarHeight = statusBarHeightLandscape;
            }

            DisplayCutoutCompat rotatedCutout = rotateCutout(
                    displayInfo.cutout, displayInfo.size.x, displayInfo.size.y, rotation, i);
            Rect insets = getSafeInsets(rotatedCutout);
            if (rotatedCutout != null && areBottomDisplayCutoutsSmallAndAtCorners(
                    getBottomDisplayCutout(rotatedCutout),
                    bounds.width(),
                    context.getResources())) {
                insets.bottom = 0;
            }
            insets.top = Math.max(insets.top, statusBarHeight);
            insets.bottom = Math.max(insets.bottom, navBarHeight);

            if (i == Surface.ROTATION_270 || i == Surface.ROTATION_180) {
                // On reverse landscape (and in rare-case when the natural orientation of the
                // device is landscape), navigation bar is on the right.
                insets.left = Math.max(insets.left, navbarWidth);
            } else {
                insets.right = Math.max(insets.right, navbarWidth);
            }
            result.add(new WindowBounds(bounds, insets, i));
        }
        return result;
    }

    static boolean UNWRAP_METHOD_AVAILABLE = true;
    static Method METHOD_DISPLAY_CUTOUT_UNWRAP = null;

    protected static Rect getBottomDisplayCutout(DisplayCutoutCompat cutout) {
        if (Utilities.ATLEAST_Q && UNWRAP_METHOD_AVAILABLE) try {
            if (METHOD_DISPLAY_CUTOUT_UNWRAP == null) {
                METHOD_DISPLAY_CUTOUT_UNWRAP = DisplayCutoutCompat.class.getDeclaredMethod("unwrap");
                METHOD_DISPLAY_CUTOUT_UNWRAP.setAccessible(true);
            }
            var displayCutout = (DisplayCutout) METHOD_DISPLAY_CUTOUT_UNWRAP.invoke(cutout);
            return displayCutout.getBoundingRectBottom();
        } catch (Exception e) {UNWRAP_METHOD_AVAILABLE = false;}
        List<Rect> rects = (cutout != null) ? cutout.getBoundingRects() : Collections.emptyList();
        Rect bottomRect = null;
        for (Rect r : rects) {
            if (bottomRect == null || r.top > bottomRect.top) {
                bottomRect = r;
                break;
            }
        }
        return bottomRect == null ? new Rect() : bottomRect;
    }

    /**
     * Wrapper around the utility method for easier emulation
     */
    protected int getDimenByName(Resources res, String resName) {
        return ResourceUtils.getDimenByName(resName, res, 0);
    }

    /**
     * Wrapper around the utility method for easier emulation
     */
    protected int getDimenByName(Resources res, String resName, String fallback) {
        int dimen = ResourceUtils.getDimenByName(resName, res, -1);
        return dimen > -1 ? dimen : getDimenByName(res, fallback);
    }

    protected boolean isGestureNav(Context context) {
        return ResourceUtils.getIntegerByName("config_navBarInteractionMode",
                context.getResources(), INVALID_RESOURCE_HANDLE) == 2;
    }

    /**
     * Returns a CachedDisplayInfo initialized for the current display
     */
    public CachedDisplayInfo getDisplayInfo(Context displayInfoContext) {
        int rotation = getRotation(displayInfoContext);
        if (Utilities.ATLEAST_S) {
            WindowMetrics windowMetrics = displayInfoContext.getSystemService(WindowManager.class)
                    .getMaximumWindowMetrics();
            return getDisplayInfo(windowMetrics, rotation);
        } else {
            Point size = new Point();
            Display display = getDisplay(displayInfoContext);
            display.getRealSize(size);
            return new CachedDisplayInfo(size, rotation);
        }
    }

    /**
     * Returns a CachedDisplayInfo initialized for the current display
     */
    protected CachedDisplayInfo getDisplayInfo(WindowMetrics windowMetrics, int rotation) {
        Point size = new Point(windowMetrics.getBounds().right, windowMetrics.getBounds().bottom);
        return new CachedDisplayInfo(size, rotation,
                WindowInsetsCompat.toWindowInsetsCompat(windowMetrics.getWindowInsets()).getDisplayCutout());
    }

    /**
     * Returns bounds of the display associated with the context, or bounds of DEFAULT_DISPLAY
     * if the context isn't associated with a display.
     */
    public Rect getCurrentBounds(Context displayInfoContext) {
        Resources res = displayInfoContext.getResources();
        Configuration config = res.getConfiguration();

        float screenWidth = config.screenWidthDp * res.getDisplayMetrics().density;
        float screenHeight = config.screenHeightDp * res.getDisplayMetrics().density;

        return new Rect(0, 0, (int) screenWidth, (int) screenHeight);
    }

    /**
     * Returns rotation of the display associated with the context, or rotation of DEFAULT_DISPLAY
     * if the context isn't associated with a display.
     */
    public int getRotation(Context displayInfoContext) {
        return getDisplay(displayInfoContext).getRotation();
    }

    /**
     * Returns the display associated with the context, or DEFAULT_DISPLAY if the context isn't
     * associated with a display.
     */
    protected Display getDisplay(Context displayInfoContext) {
        try {
            return displayInfoContext.getDisplay();
        } catch (UnsupportedOperationException e) {
            // Ignore
        }
        return displayInfoContext.getSystemService(DisplayManager.class).getDisplay(
                DEFAULT_DISPLAY);
    }

    /**
     * Returns a DisplayCutout which represents a rotated version of the original
     */
    protected DisplayCutoutCompat rotateCutout(DisplayCutoutCompat original, int startWidth, int startHeight,
            int fromRotation, int toRotation) {
        Rect safeCutout = getSafeInsets(original);
        rotateRect(safeCutout, deltaRotation(fromRotation, toRotation));
        return new DisplayCutoutCompat(androidx.core.graphics.Insets.of(safeCutout), null, null, null, null, androidx.core.graphics.Insets.NONE);
    }

    /**
     * Returns the current navigation mode from resource.
     */
    public NavigationMode getNavigationMode(Context context) {
        int modeInt = ResourceUtils.getIntegerByName(NAV_BAR_INTERACTION_MODE_RES_NAME,
                context.getResources(), INVALID_RESOURCE_HANDLE);

        if (modeInt == INVALID_RESOURCE_HANDLE) {
            Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?");
        } else {
            for (NavigationMode m : NavigationMode.values()) {
                if (m.resValue == modeInt) {
                    return m;
                }
            }
        }
        return Utilities.ATLEAST_S ? NavigationMode.NO_BUTTON : NavigationMode.THREE_BUTTONS;
    }

    /**
     * @see DisplayCutout#getSafeInsets
     */
    public static Rect getSafeInsets(DisplayCutoutCompat cutout) {
        if (Utilities.ATLEAST_Q) {
            return new Rect(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(),
                    cutout.getSafeInsetRight(), cutout.getSafeInsetBottom());
        }
        return new Rect(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(),
                cutout.getSafeInsetRight(), cutout.getSafeInsetBottom());
    }

    /** Registers a listener for Taskbar changes in Desktop Mode.  */
    public void registerDesktopVisibilityListener(DesktopVisibilityListener listener) { }

    /** Removes a previously registered listener for Taskbar changes in Desktop Mode.  */
    public void unregisterDesktopVisibilityListener(DesktopVisibilityListener listener) { }

    /** A listener for when the user enters/exits Desktop Mode.  */
    public interface DesktopVisibilityListener {
        /**
         * Called when the desktop mode state on the display whose ID is `displayId` changes.
         *
         * @param displayId The ID of the display for which this notification is triggering.
         * @param isInDesktopModeAndNotInOverview True if a desktop is currently active on the given
         *                                        display, and Overview is currently inactive.
         */
        default void onIsInDesktopModeChanged(int displayId,
                boolean isInDesktopModeAndNotInOverview) {
        }

        /**
         * Called whenever the conditions that allow the creation of desks change.
         *
         * @param canCreateDesks whether it is possible to create new desks.
         */
        default void onCanCreateDesksChanged(boolean canCreateDesks) {
        }

        /**
         * Called when a new desk is added.
         *
         * @param displayId The ID of the display on which the desk was added.
         * @param deskId The ID of the newly added desk.
         */
        default void onDeskAdded(int displayId, int deskId) {}

        /**
         * Called when an existing desk is removed.
         *
         * @param displayId The ID of the display on which the desk was removed.
         * @param deskId The ID of the desk that was removed.
         */
        default void onDeskRemoved(int displayId, int deskId) {}

        /**
         * Called when the active desk changes.
         *
         * @param displayId The ID of the display on which the desk activation change is happening.
         * @param newActiveDesk The ID of the new active desk or -1 if no desk is active anymore
         *                      (i.e. exit desktop mode).
         * @param oldActiveDesk The ID of the desk that was previously active, or -1 if no desk was
         *                      active before.
         */
        default void onActiveDeskChanged(int displayId, int newActiveDesk, int oldActiveDesk) {}
    }

}
