/*
 * Copyright (C) 2019 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.server.wm;

import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.view.InsetsSource.ID_IME;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static android.view.WindowInsets.Type.captionBar;
import static android.view.WindowInsets.Type.ime;
import static android.view.WindowInsets.Type.navigationBars;
import static android.view.WindowInsets.Type.statusBars;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_CONSUME_IME_INSETS;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE;
import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;

import android.app.StatusBarManager;
import android.content.ComponentName;
import android.graphics.Insets;
import android.graphics.Rect;
import android.os.Binder;
import android.os.RemoteException;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.view.IDisplayWindowInsetsController;
import android.view.InsetsFrameProvider;
import android.view.InsetsSource;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
import android.view.WindowInsets;
import android.view.WindowInsets.Type.InsetsType;

import androidx.test.filters.SmallTest;

import com.android.server.statusbar.StatusBarManagerInternal;
import com.android.window.flags.Flags;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

/**
 * Tests for the {@link InsetsPolicy} class.
 *
 * Build/Install/Run:
 *  atest WmTests:InsetsPolicyTest
 */
@SmallTest
@Presubmit
@RunWith(WindowTestRunner.class)
public class InsetsPolicyTest extends WindowTestsBase {

    @Before
    public void setup() {
        mWm.mAnimator.ready();
    }

    @Test
    public void testControlsForDispatch_regular() {
        addStatusBar();
        addNavigationBar();

        final InsetsSourceControl[] controls = addAppWindowAndGetControlsForDispatch();

        // The app can control both system bars.
        assertNotNull(controls);
        assertEquals(2, controls.length);
    }

    @Test
    public void testControlsForDispatch_adjacentTasksVisible() {
        addStatusBar();
        addNavigationBar();

        final Task task1 = createTask(mDisplayContent);
        final Task task2 = createTask(mDisplayContent);
        task1.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(task1, task2));
        final WindowState win = createAppWindow(task1, WINDOWING_MODE_MULTI_WINDOW, "app");
        final InsetsSourceControl[] controls = addWindowAndGetControlsForDispatch(win);

        // The app must not control any system bars.
        assertNull(controls);
    }

    @Test
    @DisableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
    public void testControlsForDispatch_freeformTaskVisible() {
        addStatusBar();
        addNavigationBar();

        final WindowState win = newWindowBuilder("app", TYPE_APPLICATION).setActivityType(
                ACTIVITY_TYPE_STANDARD).setWindowingMode(WINDOWING_MODE_FREEFORM).setDisplay(
                mDisplayContent).build();
        final InsetsSourceControl[] controls = addWindowAndGetControlsForDispatch(win);

        // The app must not control any system bars.
        assertNull(controls);
    }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
    public void testControlsForDispatch_fullscreenFreeformTaskVisible() {
        addStatusBar();
        addNavigationBar();

        final WindowState win = newWindowBuilder("app", TYPE_APPLICATION).setActivityType(
                ACTIVITY_TYPE_STANDARD).setWindowingMode(WINDOWING_MODE_FREEFORM).setDisplay(
                mDisplayContent).build();
        win.setBounds(new Rect());
        final InsetsSourceControl[] controls = addWindowAndGetControlsForDispatch(win);

        // The freeform (w/fullscreen bounds) app window can control both system bars.
        assertNotNull(controls);
        assertEquals(2, controls.length);
    }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
    public void testControlsForDispatch_nonFullscreenFreeformTaskVisible() {
        addStatusBar();
        addNavigationBar();

        final WindowState win = newWindowBuilder("app", TYPE_APPLICATION).setActivityType(
                ACTIVITY_TYPE_STANDARD).setWindowingMode(WINDOWING_MODE_FREEFORM).setDisplay(
                mDisplayContent).build();
        win.getTask().setBounds(new Rect(1, 1, 10, 10));
        final InsetsSourceControl[] controls = addWindowAndGetControlsForDispatch(win);

        // The freeform (but not fullscreen bounds) app window must not control any system bars.
        assertNull(controls);
    }

    @Test
    public void testControlsForDispatch_nonFullscreenMultiWindowTaskVisible() {
        addStatusBar();
        addNavigationBar();

        final WindowState win = newWindowBuilder("app", TYPE_APPLICATION).setActivityType(
                ACTIVITY_TYPE_STANDARD).setWindowingMode(WINDOWING_MODE_MULTI_WINDOW).setDisplay(
                mDisplayContent).build();
        win.getTask().setBounds(new Rect(1, 1, 10, 10));
        final InsetsSourceControl[] controls = addWindowAndGetControlsForDispatch(win);

        // The non fullscreen multi window app window must not control any system bars.
        assertNull(controls);
    }

    @Test
    public void testControlsForDispatch_nonFullscreenMultiWindowTaskVisible_remoteInsetControl()
            throws RemoteException {
        addStatusBar();
        addNavigationBar();
        final IDisplayWindowInsetsController insetsController = spy(
                createDisplayWindowInsetsController());
        mDisplayContent.setRemoteInsetsController(insetsController);
        mDisplayContent.getDisplayPolicy().setRemoteInsetsControllerControlsSystemBars(true);

        final WindowState win = newWindowBuilder("app", TYPE_APPLICATION).setActivityType(
                ACTIVITY_TYPE_STANDARD).setWindowingMode(WINDOWING_MODE_MULTI_WINDOW).setDisplay(
                mDisplayContent).build();
        final ComponentName component = win.mActivityRecord.mActivityComponent;
        assertNotNull(component);
        win.getTask().setBounds(new Rect(1, 1, 10, 10));
        final InsetsSourceControl[] controls = addWindowAndGetControlsForDispatch(win);

        // The remote insets controller should control the system bars.
        assertNull(controls);
        verify(insetsController).topFocusedWindowChanged(eq(component), anyInt());
    }

    @Test
    public void testControlsForDispatch_forceStatusBarVisible() {
        addStatusBar().mAttrs.forciblyShownTypes |= statusBars();
        addNavigationBar();

        final InsetsSourceControl[] controls = addAppWindowAndGetControlsForDispatch();

        // The focused app window can control both system bars.
        assertNotNull(controls);
        assertEquals(2, controls.length);
    }

    @Test
    public void testControlsForDispatch_statusBarForceShowNavigation() {
        addWindow(TYPE_NOTIFICATION_SHADE, "notificationShade").mAttrs.forciblyShownTypes |=
                navigationBars();
        addStatusBar();
        addNavigationBar();

        final InsetsSourceControl[] controls = addAppWindowAndGetControlsForDispatch();

        // The focused app window can control both system bars.
        assertNotNull(controls);
        assertEquals(2, controls.length);
    }

    @Test
    public void testControlsForDispatch_statusBarForceShowNavigation_butFocusedAnyways() {
        WindowState notifShade = addWindow(TYPE_NOTIFICATION_SHADE, "notificationShade");
        notifShade.mAttrs.forciblyShownTypes |= navigationBars();
        addNavigationBar();

        mDisplayContent.getDisplayPolicy().focusChangedLw(null, notifShade);
        InsetsSourceControl[] controls
                = mDisplayContent.getInsetsStateController().getControlsForDispatch(notifShade);

        // The app controls the navigation bar.
        assertNotNull(controls);
        assertEquals(1, controls.length);
    }

    @Test
    public void testControlsForDispatch_remoteInsetsControllerControlsBars_appHasNoControl() {
        mDisplayContent.setRemoteInsetsController(createDisplayWindowInsetsController());
        mDisplayContent.getDisplayPolicy().setRemoteInsetsControllerControlsSystemBars(true);
        addStatusBar();
        addNavigationBar();

        final InsetsSourceControl[] controls = addAppWindowAndGetControlsForDispatch();

        // The focused app window cannot control system bars.
        assertNull(controls);
    }

    @Test
    public void testControlsForDispatch_topAppHidesStatusBar() {
        addStatusBar();
        addNavigationBar();

        // Add a fullscreen (MATCH_PARENT x MATCH_PARENT) app window which hides status bar.
        final WindowState fullscreenApp = addWindow(TYPE_APPLICATION, "fullscreenApp");
        fullscreenApp.setRequestedVisibleTypes(0, WindowInsets.Type.statusBars());

        // Add a non-fullscreen dialog window.
        final WindowState dialog = addWindow(TYPE_APPLICATION, "dialog");
        dialog.mAttrs.width = WRAP_CONTENT;
        dialog.mAttrs.height = WRAP_CONTENT;

        // Let fullscreenApp be mTopFullscreenOpaqueWindowState.
        final DisplayPolicy displayPolicy = mDisplayContent.getDisplayPolicy();
        displayPolicy.beginPostLayoutPolicyLw();
        displayPolicy.applyPostLayoutPolicyLw(dialog, dialog.mAttrs, fullscreenApp, null);
        displayPolicy.applyPostLayoutPolicyLw(fullscreenApp, fullscreenApp.mAttrs, null, null);
        displayPolicy.finishPostLayoutPolicyLw();
        displayPolicy.focusChangedLw(null, dialog);

        assertEquals(fullscreenApp, displayPolicy.getTopFullscreenOpaqueWindow());

        // dialog is the focused window, but it can only control navigation bar.
        final InsetsSourceControl[] dialogControls =
                mDisplayContent.getInsetsStateController().getControlsForDispatch(dialog);
        assertNotNull(dialogControls);
        assertEquals(1, dialogControls.length);
        assertEquals(navigationBars(), dialogControls[0].getType());

        // fullscreenApp is hiding status bar, and it can keep controlling status bar.
        final InsetsSourceControl[] fullscreenAppControls =
                mDisplayContent.getInsetsStateController().getControlsForDispatch(fullscreenApp);
        assertNotNull(fullscreenAppControls);
        assertEquals(1, fullscreenAppControls.length);
        assertEquals(statusBars(), fullscreenAppControls[0].getType());

        // Assume mFocusedWindow is updated but mTopFullscreenOpaqueWindowState hasn't.
        final WindowState newFocusedFullscreenApp = addWindow(TYPE_APPLICATION, "newFullscreenApp");
        newFocusedFullscreenApp.setRequestedVisibleTypes(
                WindowInsets.Type.statusBars(), WindowInsets.Type.statusBars());
        // Make sure status bar is hidden by previous insets state.
        displayPolicy.focusChangedLw(dialog, fullscreenApp);

        final StatusBarManagerInternal sbmi =
                mDisplayContent.getDisplayPolicy().getStatusBarManagerInternal();
        clearInvocations(sbmi);
        displayPolicy.focusChangedLw(fullscreenApp, newFocusedFullscreenApp);
        // The status bar should be shown by newFocusedFullscreenApp even
        // mTopFullscreenOpaqueWindowState is still fullscreenApp.
        verify(sbmi).setWindowState(mDisplayContent.mDisplayId, StatusBarManager.WINDOW_STATUS_BAR,
                StatusBarManager.WINDOW_STATE_SHOWING);

        // Add a system window: panel.
        final WindowState panel = addWindow(TYPE_STATUS_BAR_SUB_PANEL, "panel");
        displayPolicy.focusChangedLw(newFocusedFullscreenApp, panel);

        // panel is the focused window, but it can only control navigation bar.
        // Because fullscreenApp is hiding status bar.
        InsetsSourceControl[] panelControls =
                mDisplayContent.getInsetsStateController().getControlsForDispatch(panel);
        assertNotNull(panelControls);
        assertEquals(1, panelControls.length);
        assertEquals(navigationBars(), panelControls[0].getType());

        // Add notificationShade and make it can receive keys.
        final WindowState shade = addWindow(TYPE_NOTIFICATION_SHADE, "notificationShade");
        shade.setHasSurface(true);
        assertTrue(shade.canReceiveKeys());
        mDisplayContent.getInsetsPolicy().updateBarControlTarget(panel);

        // panel can control both system bars now.
        panelControls = mDisplayContent.getInsetsStateController().getControlsForDispatch(panel);
        assertNotNull(panelControls);
        assertEquals(2, panelControls.length);

        // Make notificationShade cannot receive keys.
        shade.mAttrs.flags |= FLAG_NOT_FOCUSABLE;
        assertFalse(shade.canReceiveKeys());
        mDisplayContent.getInsetsPolicy().updateBarControlTarget(panel);

        // panel can only control navigation bar now.
        panelControls = mDisplayContent.getInsetsStateController().getControlsForDispatch(panel);
        assertNotNull(panelControls);
        assertEquals(1, panelControls.length);
        assertEquals(navigationBars(), panelControls[0].getType());
    }

    @Test
    public void testUpdateSystemBars_noForciblyControllingTypes() {
        final WindowState statusBar = addStatusBar();
        final WindowState navBar = addNavigationBar();
        final WindowState app = addWindow(TYPE_APPLICATION, "app");
        final InsetsPolicy policy = mDisplayContent.getInsetsPolicy();
        final InsetsStateController controller = mDisplayContent.getInsetsStateController();
        final InsetsSourceProvider statusBarProvider = statusBar.getControllableInsetProvider();
        final InsetsSourceProvider navBarProvider = navBar.getControllableInsetProvider();
        final InsetsSource statusBarSource = statusBar.getControllableInsetProvider().getSource();
        final InsetsSource navBarSource = navBar.getControllableInsetProvider().getSource();

        makeWindowVisible(statusBar, navBar, app);
        statusBarProvider.setServerVisible(true);
        navBarProvider.setServerVisible(true);

        // The app hides both bars.
        app.setRequestedVisibleTypes(
                0, statusBars() | navigationBars());

        policy.updateSystemBars(
                app,
                0 /* displayForciblyShowingTypes */,
                0 /* displayForciblyHidingTypes */,
                false /* showSystemBarsByLegacyPolicy */);

        statusBarProvider.updateClientVisibility(statusBarProvider.getControlTarget(), null);
        navBarProvider.updateClientVisibility(navBarProvider.getControlTarget(), null);

        assertFalse("statusBars must not be forcibly shown.",
                policy.areTypesForciblyShown(statusBars()));
        assertFalse("navigationBars must not be forcibly shown.",
                policy.areTypesForciblyShown(navigationBars()));
        assertFalse("statusBars must not be forcibly hidden.",
                policy.areTypesForciblyHidden(statusBars()));
        assertFalse("navigationBars must not be forcibly hidden.",
                policy.areTypesForciblyHidden(navigationBars()));
        assertFalse("statusBars must not be forcibly consumed.",
                statusBarSource.hasFlags(InsetsSource.FLAG_FORCE_CONSUMING));
        assertFalse("navigationBars must not be forcibly consumed.",
                navBarSource.hasFlags(InsetsSource.FLAG_FORCE_CONSUMING));
        assertFalse("statusBars must not be visible.", statusBarSource.isVisible());
        assertFalse("navigationBars must not be visible.", navBarSource.isVisible());

        final InsetsSourceControl[] controls = controller.getControlsForDispatch(app);

        // The app should receive both controls.
        assertNotNull(controls);
        assertEquals(2, controls.length);
    }

    @Test
    public void testUpdateSystemBars_forciblyShowingStatusBars() {
        final WindowState statusBar = addStatusBar();
        final WindowState navBar = addNavigationBar();
        final WindowState app = addWindow(TYPE_APPLICATION, "app");
        final InsetsPolicy policy = mDisplayContent.getInsetsPolicy();
        final InsetsStateController controller = mDisplayContent.getInsetsStateController();
        final InsetsSourceProvider statusBarProvider = statusBar.getControllableInsetProvider();
        final InsetsSourceProvider navBarProvider = navBar.getControllableInsetProvider();
        final InsetsSource statusBarSource = statusBar.getControllableInsetProvider().getSource();
        final InsetsSource navBarSource = navBar.getControllableInsetProvider().getSource();

        makeWindowVisible(statusBar, navBar, app);
        statusBarProvider.setServerVisible(true);
        navBarProvider.setServerVisible(true);

        // The app hides both bars.
        app.setRequestedVisibleTypes(
                0, statusBars() | navigationBars());

        policy.updateSystemBars(
                app,
                statusBars() /* displayForciblyShowingTypes */,
                0 /* displayForciblyHidingTypes */,
                false /* showSystemBarsByLegacyPolicy */);

        statusBarProvider.updateClientVisibility(statusBarProvider.getControlTarget(), null);
        navBarProvider.updateClientVisibility(navBarProvider.getControlTarget(), null);

        assertTrue("statusBars must be forcibly shown.",
                policy.areTypesForciblyShown(statusBars()));
        assertFalse("navigationBars must not be forcibly shown.",
                policy.areTypesForciblyShown(navigationBars()));
        assertFalse("statusBars must not be forcibly hidden.",
                policy.areTypesForciblyHidden(statusBars()));
        assertFalse("navigationBars must not be forcibly hidden.",
                policy.areTypesForciblyHidden(navigationBars()));
        assertTrue("statusBars must be forcibly consumed.",
                statusBarSource.hasFlags(InsetsSource.FLAG_FORCE_CONSUMING));
        assertFalse("navigationBars must not be forcibly consumed.",
                navBarSource.hasFlags(InsetsSource.FLAG_FORCE_CONSUMING));
        assertTrue("statusBars must be visible.", statusBarSource.isVisible());
        assertFalse("navigationBars must not be visible.", navBarSource.isVisible());

        final InsetsSourceControl[] controls = controller.getControlsForDispatch(app);

        // The app should not receive the status bar control.
        assertNotNull(controls);
        assertEquals(1, controls.length);
        assertNotEquals(controls[0].getType(), statusBars());
    }

    @Test
    public void testUpdateSystemBars_forciblyHidingStatusBars() {
        final WindowState statusBar = addStatusBar();
        final WindowState navBar = addNavigationBar();
        final WindowState app = addWindow(TYPE_APPLICATION, "app");
        final InsetsPolicy policy = mDisplayContent.getInsetsPolicy();
        final InsetsStateController controller = mDisplayContent.getInsetsStateController();
        final InsetsSourceProvider statusBarProvider = statusBar.getControllableInsetProvider();
        final InsetsSourceProvider navBarProvider = navBar.getControllableInsetProvider();
        final InsetsSource statusBarSource = statusBar.getControllableInsetProvider().getSource();
        final InsetsSource navBarSource = navBar.getControllableInsetProvider().getSource();

        makeWindowVisible(statusBar, navBar, app);
        statusBarProvider.setServerVisible(true);
        navBarProvider.setServerVisible(true);

        // The app shows both bars.
        app.setRequestedVisibleTypes(
                statusBars() | navigationBars(), statusBars() | navigationBars());

        policy.updateSystemBars(
                app,
                0 /* displayForciblyShowingTypes */,
                statusBars() /* displayForciblyHidingTypes */,
                false /* showSystemBarsByLegacyPolicy */);

        statusBarProvider.updateClientVisibility(statusBarProvider.getControlTarget(), null);
        navBarProvider.updateClientVisibility(navBarProvider.getControlTarget(), null);

        assertFalse("statusBars must not be forcibly shown.",
                policy.areTypesForciblyShown(statusBars()));
        assertFalse("navigationBars must not be forcibly shown.",
                policy.areTypesForciblyShown(navigationBars()));
        assertTrue("statusBars must be forcibly hidden.",
                policy.areTypesForciblyHidden(statusBars()));
        assertFalse("navigationBars must not be forcibly hidden.",
                policy.areTypesForciblyHidden(navigationBars()));
        assertFalse("statusBars must not be forcibly consumed.",
                statusBarSource.hasFlags(InsetsSource.FLAG_FORCE_CONSUMING));
        assertFalse("navigationBars must not be forcibly consumed.",
                navBarSource.hasFlags(InsetsSource.FLAG_FORCE_CONSUMING));
        assertFalse("statusBars must not be visible.", statusBarSource.isVisible());
        assertTrue("navigationBars must be visible.", navBarSource.isVisible());

        final InsetsSourceControl[] controls = controller.getControlsForDispatch(app);

        // The app should not receive the status bar control.
        assertNotNull(controls);
        assertEquals(1, controls.length);
        assertNotEquals(controls[0].getType(), statusBars());
    }

    @Test
    public void testUpdateSystemBars_inSplitScreenMode() {
        final WindowState statusBar = addStatusBar();
        final WindowState navBar = addNavigationBar();
        final WindowState app = addWindow(TYPE_APPLICATION, "app");
        final InsetsPolicy policy = mDisplayContent.getInsetsPolicy();
        final InsetsStateController controller = mDisplayContent.getInsetsStateController();
        final InsetsSourceProvider statusBarProvider = statusBar.getControllableInsetProvider();
        final InsetsSourceProvider navBarProvider = navBar.getControllableInsetProvider();
        final InsetsSource statusBarSource = statusBar.getControllableInsetProvider().getSource();
        final InsetsSource navBarSource = navBar.getControllableInsetProvider().getSource();

        makeWindowVisible(statusBar, navBar, app);
        statusBarProvider.setServerVisible(true);
        navBarProvider.setServerVisible(true);

        // The app hides both bars.
        app.setRequestedVisibleTypes(
                0, statusBars() | navigationBars());

        policy.updateSystemBars(
                app,
                0 /* displayForciblyShowingTypes */,
                0 /* displayForciblyHidingTypes */,
                true /* showSystemBarsByLegacyPolicy */);

        statusBarProvider.updateClientVisibility(statusBarProvider.getControlTarget(), null);
        navBarProvider.updateClientVisibility(navBarProvider.getControlTarget(), null);

        assertTrue("statusBars must be forcibly shown.",
                policy.areTypesForciblyShown(statusBars()));
        assertTrue("navigationBars must be forcibly shown.",
                policy.areTypesForciblyShown(navigationBars()));
        assertFalse("statusBars must not be forcibly hidden.",
                policy.areTypesForciblyHidden(statusBars()));
        assertFalse("navigationBars must not be forcibly hidden.",
                policy.areTypesForciblyHidden(navigationBars()));
        assertTrue("statusBars must be forcibly consumed.",
                statusBarSource.hasFlags(InsetsSource.FLAG_FORCE_CONSUMING));
        assertTrue("navigationBars must be forcibly consumed.",
                navBarSource.hasFlags(InsetsSource.FLAG_FORCE_CONSUMING));
        assertTrue("statusBars must be visible.", statusBarSource.isVisible());
        assertTrue("navigationBars must be visible.", navBarSource.isVisible());

        final InsetsSourceControl[] controls = controller.getControlsForDispatch(app);

        // The app should not receive any control.
        assertNull(controls);
    }

    @Test
    public void testUpdateSystemBars_inNonFullscreenFreeformMode() {
        final WindowState statusBar = addStatusBar();
        final WindowState navBar = addNavigationBar();
        final WindowState app = addWindow(TYPE_APPLICATION, "app");
        final InsetsPolicy policy = mDisplayContent.getInsetsPolicy();
        final InsetsStateController controller = mDisplayContent.getInsetsStateController();
        final InsetsSourceProvider statusBarProvider = statusBar.getControllableInsetProvider();
        final InsetsSourceProvider navBarProvider = navBar.getControllableInsetProvider();
        final InsetsSource statusBarSource = statusBar.getControllableInsetProvider().getSource();
        final InsetsSource navBarSource = navBar.getControllableInsetProvider().getSource();

        makeWindowVisible(statusBar, navBar, app);
        statusBarProvider.setServerVisible(true);
        navBarProvider.setServerVisible(true);

        // The app hides both bars.
        app.setRequestedVisibleTypes(
                0, statusBars() | navigationBars());

        policy.updateSystemBars(
                app,
                0 /* displayForciblyShowingTypes */,
                0 /* displayForciblyHidingTypes */,
                true /* showSystemBarsByLegacyPolicy */);

        statusBarProvider.updateClientVisibility(statusBarProvider.getControlTarget(), null);
        navBarProvider.updateClientVisibility(navBarProvider.getControlTarget(), null);

        assertTrue("statusBars must be forcibly shown.",
                policy.areTypesForciblyShown(statusBars()));
        assertTrue("navigationBars must be forcibly shown.",
                policy.areTypesForciblyShown(navigationBars()));
        assertFalse("statusBars must not be forcibly hidden.",
                policy.areTypesForciblyHidden(statusBars()));
        assertFalse("navigationBars must not be forcibly hidden.",
                policy.areTypesForciblyHidden(navigationBars()));
        assertTrue("statusBars must be forcibly consumed.",
                statusBarSource.hasFlags(InsetsSource.FLAG_FORCE_CONSUMING));
        assertTrue("navigationBars must be forcibly consumed.",
                navBarSource.hasFlags(InsetsSource.FLAG_FORCE_CONSUMING));
        assertTrue("statusBars must be visible.", statusBarSource.isVisible());
        assertTrue("navigationBars must be visible.", navBarSource.isVisible());

        final InsetsSourceControl[] controls = controller.getControlsForDispatch(app);

        // The app should not receive any control.
        assertNull(controls);
    }

    @Test
    public void testUpdateSystemBars_forciblyHidingStatusBars_inSplitScreenMode() {
        final WindowState statusBar = addStatusBar();
        final WindowState navBar = addNavigationBar();
        final WindowState app = addWindow(TYPE_APPLICATION, "app");
        final InsetsPolicy policy = mDisplayContent.getInsetsPolicy();
        final InsetsStateController controller = mDisplayContent.getInsetsStateController();
        final InsetsSourceProvider statusBarProvider = statusBar.getControllableInsetProvider();
        final InsetsSourceProvider navBarProvider = navBar.getControllableInsetProvider();
        final InsetsSource statusBarSource = statusBar.getControllableInsetProvider().getSource();
        final InsetsSource navBarSource = navBar.getControllableInsetProvider().getSource();

        makeWindowVisible(statusBar, navBar, app);
        statusBarProvider.setServerVisible(true);
        navBarProvider.setServerVisible(true);

        // The app shows both bars.
        app.setRequestedVisibleTypes(
                statusBars() | navigationBars(), statusBars() | navigationBars());

        policy.updateSystemBars(
                app,
                0 /* displayForciblyShowingTypes */,
                statusBars() /* displayForciblyHidingTypes */,
                true /* showSystemBarsByLegacyPolicy */);

        statusBarProvider.updateClientVisibility(statusBarProvider.getControlTarget(), null);
        navBarProvider.updateClientVisibility(navBarProvider.getControlTarget(), null);

        assertFalse("statusBars must not be forcibly shown.",
                policy.areTypesForciblyShown(statusBars()));
        assertFalse("navigationBars must not be forcibly shown.",
                policy.areTypesForciblyShown(navigationBars()));
        assertTrue("statusBars must be forcibly hidden.",
                policy.areTypesForciblyHidden(statusBars()));
        assertFalse("navigationBars must not be forcibly hidden.",
                policy.areTypesForciblyHidden(navigationBars()));
        assertFalse("statusBars must not be forcibly consumed.",
                statusBarSource.hasFlags(InsetsSource.FLAG_FORCE_CONSUMING));
        assertFalse("navigationBars must not be forcibly consumed.",
                navBarSource.hasFlags(InsetsSource.FLAG_FORCE_CONSUMING));
        assertFalse("statusBars must not be visible.", statusBarSource.isVisible());
        assertTrue("navigationBars must be visible.", navBarSource.isVisible());

        final InsetsSourceControl[] controls = controller.getControlsForDispatch(app);

        // The app should not receive the status bar control.
        assertNotNull(controls);
        assertEquals(1, controls.length);
        assertNotEquals(controls[0].getType(), statusBars());
    }

    @SetupWindows(addWindows = W_ACTIVITY)
    @Test
    public void testShowTransientBars_bothCanBeTransient_appGetsBothFakeControls() {
        final WindowState statusBar = addStatusBar();
        final InsetsSourceProvider statusBarProvider = statusBar.getControllableInsetProvider();
        final int statusBarId = statusBarProvider.getSource().getId();
        statusBar.setHasSurface(true);
        statusBarProvider.setServerVisible(true);
        final WindowState navBar = addNavigationBar();
        final InsetsSourceProvider navBarProvider = statusBar.getControllableInsetProvider();
        final int navBarId = statusBarProvider.getSource().getId();
        navBar.setHasSurface(true);
        navBarProvider.setServerVisible(true);
        final InsetsPolicy policy = mDisplayContent.getInsetsPolicy();

        // Make both system bars invisible.
        mAppWindow.setRequestedVisibleTypes(
                0, navigationBars() | statusBars());
        mDisplayContent.getDisplayPolicy().focusChangedLw(null, mAppWindow);
        waitUntilWindowAnimatorIdle();
        assertFalse(mDisplayContent.getInsetsStateController().getRawInsetsState()
                .isSourceOrDefaultVisible(statusBarId, statusBars()));
        assertFalse(mDisplayContent.getInsetsStateController().getRawInsetsState()
                .isSourceOrDefaultVisible(navBarId, navigationBars()));

        policy.showTransient(navigationBars() | statusBars(), true /* isGestureOnSystemBar */);
        waitUntilWindowAnimatorIdle();
        final InsetsSourceControl[] controls =
                mDisplayContent.getInsetsStateController().getControlsForDispatch(mAppWindow);

        // The app must get both fake controls.
        assertEquals(2, controls.length);
        for (int i = controls.length - 1; i >= 0; i--) {
            assertNull(controls[i].getLeash());
        }

        assertTrue(mDisplayContent.getInsetsStateController().getRawInsetsState()
                .isSourceOrDefaultVisible(statusBarId, statusBars()));
        assertTrue(mDisplayContent.getInsetsStateController().getRawInsetsState()
                .isSourceOrDefaultVisible(navBarId, navigationBars()));
    }

    @SetupWindows(addWindows = W_ACTIVITY)
    @Test
    public void testShowTransientBars_statusBarCanBeTransient_appGetsStatusBarFakeControl() {
        addStatusBar().getControllableInsetProvider().getSource().setVisible(false);
        addNavigationBar().getControllableInsetProvider().setServerVisible(true);

        mDisplayContent.getDisplayPolicy().focusChangedLw(null, mAppWindow);
        final InsetsPolicy policy = mDisplayContent.getInsetsPolicy();
        policy.showTransient(navigationBars() | statusBars(),
                true /* isGestureOnSystemBar */);
        waitUntilWindowAnimatorIdle();
        assertTrue(policy.isTransient(statusBars()));
        assertFalse(policy.isTransient(navigationBars()));
        final InsetsSourceControl[] controls =
                mDisplayContent.getInsetsStateController().getControlsForDispatch(mAppWindow);

        // The app must get the fake control of the status bar, and must get the real control of the
        // navigation bar.
        assertEquals(2, controls.length);
        for (int i = controls.length - 1; i >= 0; i--) {
            final InsetsSourceControl control = controls[i];
            if (control.getType() == statusBars()) {
                assertNull(controls[i].getLeash());
            } else {
                assertNotNull(controls[i].getLeash());
            }
        }
    }

    @SetupWindows(addWindows = W_ACTIVITY)
    @Test
    public void testAbortTransientBars_bothCanBeAborted_appGetsBothRealControls() {
        final InsetsSource statusBarSource =
                addStatusBar().getControllableInsetProvider().getSource();
        final InsetsSource navBarSource =
                addNavigationBar().getControllableInsetProvider().getSource();
        statusBarSource.setVisible(false);
        navBarSource.setVisible(false);
        mAppWindow.setRequestedVisibleTypes(0, navigationBars() | statusBars());
        mAppWindow.mAboveInsetsState.addSource(navBarSource);
        mAppWindow.mAboveInsetsState.addSource(statusBarSource);
        mDisplayContent.getDisplayPolicy().focusChangedLw(null, mAppWindow);
        final InsetsPolicy policy = mDisplayContent.getInsetsPolicy();
        policy.showTransient(navigationBars() | statusBars(),
                true /* isGestureOnSystemBar */);
        waitUntilWindowAnimatorIdle();
        InsetsSourceControl[] controls =
                mDisplayContent.getInsetsStateController().getControlsForDispatch(mAppWindow);

        // The app must get both fake controls.
        assertEquals(2, controls.length);
        for (int i = controls.length - 1; i >= 0; i--) {
            assertNull(controls[i].getLeash());
        }

        final InsetsState state = mAppWindow.getInsetsState();
        state.setSourceVisible(statusBarSource.getId(), true);
        state.setSourceVisible(navBarSource.getId(), true);

        final InsetsState clientState = mAppWindow.getInsetsState();
        // The transient bar states for client should be invisible.
        assertFalse(clientState.isSourceOrDefaultVisible(statusBarSource.getId(), statusBars()));
        assertFalse(clientState.isSourceOrDefaultVisible(navBarSource.getId(), navigationBars()));
        // The original state shouldn't be modified.
        assertTrue(state.isSourceOrDefaultVisible(statusBarSource.getId(), statusBars()));
        assertTrue(state.isSourceOrDefaultVisible(navBarSource.getId(), navigationBars()));

        final @InsetsType int changedTypes = mAppWindow.setRequestedVisibleTypes(
                navigationBars() | statusBars(), navigationBars() | statusBars());
        policy.onRequestedVisibleTypesChanged(mAppWindow, changedTypes, null /* statsToken */);
        waitUntilWindowAnimatorIdle();

        controls = mDisplayContent.getInsetsStateController().getControlsForDispatch(mAppWindow);

        // The app must get both real controls.
        assertEquals(2, controls.length);
        for (int i = controls.length - 1; i >= 0; i--) {
            assertNotNull(controls[i].getLeash());
        }
    }

    @Test
    public void testShowTransientBars_abortsWhenControlTargetChanges() {
        addStatusBar().getControllableInsetProvider().getSource().setVisible(false);
        addNavigationBar().getControllableInsetProvider().getSource().setVisible(false);
        final WindowState app = addWindow(TYPE_APPLICATION, "app");
        final WindowState app2 = addWindow(TYPE_APPLICATION, "app");

        mDisplayContent.getDisplayPolicy().focusChangedLw(null, app);
        final InsetsPolicy policy = mDisplayContent.getInsetsPolicy();
        policy.showTransient(navigationBars() | statusBars(), true /* isGestureOnSystemBar */);
        mDisplayContent.getDisplayPolicy().focusChangedLw(app, app2);
        assertFalse(policy.isTransient(statusBars()));
        assertFalse(policy.isTransient(navigationBars()));
    }

    @Test
    public void testFakeControlTarget_overrideVisibilityReceivedByWindows() {
        final WindowState statusBar = addStatusBar();
        final InsetsSourceProvider statusBarProvider = statusBar.getControllableInsetProvider();
        statusBar.mSession.mCanForceShowingInsets = true;
        statusBar.setHasSurface(true);
        statusBarProvider.setServerVisible(true);

        final InsetsSource statusBarSource = statusBarProvider.getSource();
        final int statusBarId = statusBarSource.getId();
        assertTrue(statusBarSource.isVisible());

        final WindowState app1 = addWindow(TYPE_APPLICATION, "app1");
        app1.mAboveInsetsState.addSource(statusBarSource);
        assertTrue(app1.getInsetsState().peekSource(statusBarId).isVisible());

        final WindowState app2 = addWindow(TYPE_APPLICATION, "app2");
        app2.mAboveInsetsState.addSource(statusBarSource);
        assertTrue(app2.getInsetsState().peekSource(statusBarId).isVisible());

        // Let app2 be the focused window. Otherwise, the control target could be overwritten by
        // DisplayPolicy#updateSystemBarAttributes unexpectedly.
        mDisplayContent.getDisplayPolicy().focusChangedLw(null, app2);

        app2.setRequestedVisibleTypes(0, navigationBars() | statusBars());
        mDisplayContent.getInsetsPolicy().updateBarControlTarget(app2);
        waitUntilWindowAnimatorIdle();

        // app2 is the real control target now. It can override the visibility of all sources that
        // it controls.
        assertFalse(statusBarSource.isVisible());
        assertFalse(app1.getInsetsState().peekSource(statusBarId).isVisible());
        assertFalse(app2.getInsetsState().peekSource(statusBarId).isVisible());

        statusBar.mAttrs.forciblyShownTypes = statusBars();
        mDisplayContent.getDisplayPolicy().applyPostLayoutPolicyLw(
                statusBar, statusBar.mAttrs, null, null);
        mDisplayContent.getInsetsPolicy().updateBarControlTarget(app2);
        waitUntilWindowAnimatorIdle();

        // app2 is the fake control target now. It can only override the visibility of sources
        // received by windows, but not the raw source.
        assertTrue(statusBarSource.isVisible());
        assertFalse(app1.getInsetsState().peekSource(statusBarId).isVisible());
        assertFalse(app2.getInsetsState().peekSource(statusBarId).isVisible());

    }

    @SetupWindows(addWindows = W_INPUT_METHOD)
    @Test
    public void testConsumeImeInsets() {
        final DisplayPolicy displayPolicy = mDisplayContent.getDisplayPolicy();
        final InsetsSource imeSource = new InsetsSource(ID_IME, ime());
        imeSource.setVisible(true);
        mImeWindow.mHasSurface = true;

        final WindowState win1 = addWindow(TYPE_APPLICATION, "win1");
        final WindowState win2 = addWindow(TYPE_APPLICATION, "win2");

        win1.mAboveInsetsState.addSource(imeSource);
        win1.mHasSurface = true;
        win2.mAboveInsetsState.addSource(imeSource);
        win2.mHasSurface = true;

        assertTrue(mImeWindow.isVisible());
        assertTrue(win1.isVisible());
        assertTrue(win2.isVisible());

        // Make sure both windows have visible IME insets.
        assertTrue(win1.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
        assertTrue(win2.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));

        win2.mAttrs.privateFlags |= PRIVATE_FLAG_CONSUME_IME_INSETS;

        displayPolicy.beginPostLayoutPolicyLw();
        displayPolicy.applyPostLayoutPolicyLw(win2, win2.mAttrs, null, null);
        displayPolicy.applyPostLayoutPolicyLw(win1, win1.mAttrs, null, null);
        displayPolicy.finishPostLayoutPolicyLw();

        // Make sure win2 doesn't have visible IME insets, but win1 still does.
        assertTrue(win2.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
        assertFalse(win1.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
        assertTrue(win1.getWindowFrames().hasInsetsChanged());

        win2.mAttrs.privateFlags &= ~PRIVATE_FLAG_CONSUME_IME_INSETS;
        win2.getWindowFrames().setInsetsChanged(false);

        displayPolicy.beginPostLayoutPolicyLw();
        displayPolicy.applyPostLayoutPolicyLw(win2, win2.mAttrs, null, null);
        displayPolicy.applyPostLayoutPolicyLw(win1, win1.mAttrs, null, null);
        displayPolicy.finishPostLayoutPolicyLw();

        // Make sure both windows have visible IME insets.
        assertTrue(win1.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
        assertTrue(win2.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
        assertTrue(win1.getWindowFrames().hasInsetsChanged());
    }

    /**
     * This test verifies that after setting {@link WindowContainer#mExcludeInsetsTypes}, the IME
     * insets have a height of zero (applied in {@link InsetsPolicy#adjustVisibilityForIme}).
     */
    @SetupWindows(addWindows = W_INPUT_METHOD)
    @Test
    public void testExcludeImeInsets() {
        final DisplayPolicy displayPolicy = mDisplayContent.getDisplayPolicy();
        final InsetsSource imeSource = new InsetsSource(ID_IME, ime());
        imeSource.setVisible(true);
        mImeWindow.mHasSurface = true;

        final WindowState win = addWindow(TYPE_APPLICATION, "win1");
        win.setRequestedVisibleTypes(0, ime());

        win.mAboveInsetsState.addSource(imeSource);
        win.mHasSurface = true;

        DisplayContentTests.performLayout(mDisplayContent);
        // IME should cover half of the app's window
        final var winFrame = win.getFrame();
        imeSource.setFrame(winFrame.left, winFrame.bottom / 2, winFrame.right, winFrame.bottom);
        imeSource.setVisibleFrame(imeSource.getFrame());
        DisplayContentTests.performLayout(mDisplayContent);

        assertTrue(mImeWindow.isVisible());
        assertTrue(win.isVisible());

        displayPolicy.beginPostLayoutPolicyLw();
        displayPolicy.applyPostLayoutPolicyLw(win, win.mAttrs, null, null);
        displayPolicy.finishPostLayoutPolicyLw();

        final var imeInsetsShown = win.getInsetsState().calculateInsets(win.getFrame(),
                win.getBounds(), ime(), true);
        assertEquals(new Rect(0, 0, 0, winFrame.bottom / 2), imeInsetsShown.toRect());


        // Now we're setting the excludedInsetsTypes for the IME. The IME is still showing, but
        // in this case, InsetsPolicy#adjustVisibilityForIme will override and dispatch IME
        // insets with zero height.
        win.setExcludeInsetsTypes(ime());

        displayPolicy.beginPostLayoutPolicyLw();
        displayPolicy.applyPostLayoutPolicyLw(win, win.mAttrs, null, null);
        displayPolicy.finishPostLayoutPolicyLw();

        final var imeInsetsHidden = win.getInsetsState().calculateInsets(win.getFrame(),
                win.getBounds(), ime(), true);
        assertEquals(Insets.NONE, imeInsetsHidden);
    }

    @Test
    public void testEnforceInsetsPolicyForTarget_imeInsetsForFreeform() {
        final InsetsPolicy policy = mDisplayContent.getInsetsPolicy();
        final InsetsState originalState = new InsetsState();
        final int captionBarId = InsetsSource.createId(null, 0, captionBar());
        final InsetsSource captionBarSource = new InsetsSource(captionBarId, captionBar());
        final InsetsSource imeSource = new InsetsSource(ID_IME, ime());
        imeSource.setVisible(true);
        originalState.addSource(imeSource);
        originalState.addSource(captionBarSource);

        // Create two windows in the same freeform task.
        final Task freeformTask = createTask(mDisplayContent, WINDOWING_MODE_FREEFORM,
                ACTIVITY_TYPE_STANDARD);
        final WindowState imeTargetWindow = createAppWindow(freeformTask, TYPE_APPLICATION,
                "imeTarget");
        final WindowState anotherWindowInSameTask = createAppWindow(freeformTask, TYPE_APPLICATION,
                "anotherWindow");

        // Create a window in a different freeform task.
        final Task anotherFreeformTask = createTask(mDisplayContent, WINDOWING_MODE_FREEFORM,
                ACTIVITY_TYPE_STANDARD);
        final WindowState windowInDifferentTask = createAppWindow(
                anotherFreeformTask, TYPE_APPLICATION, "windowInDifferentTask");

        // Set the IME target and request IME visibility.
        mDisplayContent.setImeInputTarget(imeTargetWindow);
        imeTargetWindow.setRequestedVisibleTypes(ime(), ime());

        // Case 1: A window in the same task as the IME target should receive IME insets.
        InsetsState resultState = policy.enforceInsetsPolicyForTarget(
                anotherWindowInSameTask, originalState);
        assertEquals(
                "Window in same task should get IME insets",
                imeSource, resultState.peekSource(ID_IME));
        // It should also keep other insets like caption bar for floating windows.
        resultState = policy.enforceInsetsPolicyForTarget(
                anotherWindowInSameTask, originalState);
        assertEquals("Window in same task should still get IME insets",
                imeSource, resultState.peekSource(ID_IME));
        assertEquals("Floating window should keep caption bar insets",
                captionBarSource, resultState.peekSource(captionBarId));

        // Case 2: The IME target window itself should receive IME insets.
        resultState = policy.enforceInsetsPolicyForTarget(imeTargetWindow, originalState);
        assertEquals("IME target window should get IME insets",
                imeSource, resultState.peekSource(ID_IME));

        // Case 3: A window in a different task should NOT receive IME insets.
        resultState = policy.enforceInsetsPolicyForTarget(windowInDifferentTask, originalState);
        assertNull("Window in different task should not get IME insets",
                resultState.peekSource(ID_IME));

        // Case 4: Pinned (PIP) window should not receive IME insets.
        final Task pinnedTask = createTask(mDisplayContent, WINDOWING_MODE_PINNED,
                ACTIVITY_TYPE_STANDARD);
        final WindowState pinnedWindow = createAppWindow(pinnedTask, TYPE_APPLICATION,
                "pinnedWindow");
        mDisplayContent.setImeInputTarget(pinnedWindow);
        resultState = policy.enforceInsetsPolicyForTarget(pinnedWindow, originalState);
        assertNull("Pinned window should not get IME insets", resultState.peekSource(ID_IME));
    }


    private WindowState addNavigationBar() {
        final Binder owner = new Binder();
        final WindowState win = newWindowBuilder("navBar", TYPE_NAVIGATION_BAR).build();
        win.mAttrs.flags |= FLAG_NOT_FOCUSABLE;
        win.mAttrs.providedInsets = new InsetsFrameProvider[] {
                new InsetsFrameProvider(owner, 0, WindowInsets.Type.navigationBars()),
                new InsetsFrameProvider(owner, 0, WindowInsets.Type.tappableElement()),
                new InsetsFrameProvider(owner, 0, WindowInsets.Type.mandatorySystemGestures())
        };
        mDisplayContent.getDisplayPolicy().addWindowLw(win, win.mAttrs);
        return win;
    }

    private WindowState addStatusBar() {
        final Binder owner = new Binder();
        final WindowState win = newWindowBuilder("statusBar", TYPE_STATUS_BAR).build();
        win.mAttrs.flags |= FLAG_NOT_FOCUSABLE;
        win.mAttrs.providedInsets = new InsetsFrameProvider[] {
                new InsetsFrameProvider(owner, 0, WindowInsets.Type.statusBars()),
                new InsetsFrameProvider(owner, 0, WindowInsets.Type.tappableElement()),
                new InsetsFrameProvider(owner, 0, WindowInsets.Type.mandatorySystemGestures())
        };
        mDisplayContent.getDisplayPolicy().addWindowLw(win, win.mAttrs);
        return win;
    }

    private WindowState addWindow(int type, String name) {
        final WindowState win = newWindowBuilder(name, type).build();
        mDisplayContent.getDisplayPolicy().addWindowLw(win, win.mAttrs);
        return win;
    }

    private InsetsSourceControl[] addAppWindowAndGetControlsForDispatch() {
        return addWindowAndGetControlsForDispatch(addWindow(TYPE_APPLICATION, "app"));
    }

    private InsetsSourceControl[] addWindowAndGetControlsForDispatch(WindowState win) {
        mDisplayContent.getDisplayPolicy().addWindowLw(win, win.mAttrs);
        // Force update the focus in DisplayPolicy here. Otherwise, without server side focus
        // update, the policy relying on windowing type will never get updated.
        mDisplayContent.getDisplayPolicy().focusChangedLw(null, win);
        return mDisplayContent.getInsetsStateController().getControlsForDispatch(win);
    }
}
