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

import static android.content.res.Configuration.UI_MODE_TYPE_DESK;
import static android.content.res.Configuration.UI_MODE_TYPE_MASK;
import static android.content.res.Configuration.UI_MODE_TYPE_NORMAL;
import static android.server.wm.app.Components.TEST_ACTIVITY;
import static android.server.wm.deskresources.Components.DESK_RESOURCES_ACTIVITY;

import static com.android.compatibility.common.util.SystemUtil.runShellCommand;

import static org.junit.Assume.assumeTrue;

import android.content.ComponentName;
import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.platform.test.annotations.Presubmit;
import android.view.Surface;

import org.junit.Test;

/**
 * Build/Install/Run:
 * atest CtsWindowManagerDeviceTestCases:DockConfigChangeTests
 */
@Presubmit
public class DockConfigChangeTests extends ActivityManagerTestBase {

    @Test
    public void testDeskMode_noConfigChange() {
        // Test only applies to behavior when the config_skipActivityRelaunchWhenDocking flag is
        // enabled.
        assumeTrue(getConfigSkipRelaunchOnDock());

        RotationSession rotationSession = createManagedRotationSession();

        launchActivity(TEST_ACTIVITY);
        waitAndAssertResumedActivity(TEST_ACTIVITY, "Activity must be resumed");

        // Set rotation to the same rotation as the device would be rotated to after docking. This
        // prevents an extraneous config change from the device rotating when docked/undocked.
        rotateToDockRotation(rotationSession);
        waitAndAssertResumedActivity(TEST_ACTIVITY, "Activity must be resumed");
        separateTestJournal();

        final DockTestSession dockTestSession = mObjectTracker.manage(new DockTestSession());

        // Dock the device.
        dockTestSession.dock();
        dockTestSession.waitForDeskUiMode(TEST_ACTIVITY);

        // The activity receives a configuration change instead of relaunching.
        assertRelaunchOrConfigChanged(TEST_ACTIVITY, 0 /* numRelaunch */, 1 /* numConfigChange */);

        // Undock the device.
        separateTestJournal();
        dockTestSession.undock();
        dockTestSession.waitForNormalUiMode(TEST_ACTIVITY);

        // The activity receives another configuration change.
        assertRelaunchOrConfigChanged(TEST_ACTIVITY, 0 /* numRelaunch */, 1 /* numConfigChange */);
    }

    @Test
    public void testDeskMode_hasDeskResources_relaunches() {
        // Test only applies to behavior when the config_skipActivityRelaunchWhenDocking flag is
        // enabled.
        assumeTrue(getConfigSkipRelaunchOnDock());

        RotationSession rotationSession = createManagedRotationSession();

        launchActivity(DESK_RESOURCES_ACTIVITY);
        waitAndAssertResumedActivity(DESK_RESOURCES_ACTIVITY, "Activity must be resumed");

        // Set rotation to the same rotation as the device would be rotated to after docking. This
        // prevents an extraneous config change from the device rotating when docked/undocked.
        rotateToDockRotation(rotationSession);
        waitAndAssertResumedActivity(DESK_RESOURCES_ACTIVITY, "Activity must be resumed");
        separateTestJournal();

        final DockTestSession dockTestSession = mObjectTracker.manage(new DockTestSession());

        // Dock the device.
        dockTestSession.dock();
        dockTestSession.waitForDeskUiMode(DESK_RESOURCES_ACTIVITY);

        // The activity is relaunched since the app has -desk resources.
        assertRelaunchOrConfigChanged(DESK_RESOURCES_ACTIVITY, 1 /* numRelaunch */,
                0 /* numConfigChange */);

        // Undock the device.
        separateTestJournal();
        dockTestSession.undock();
        dockTestSession.waitForNormalUiMode(DESK_RESOURCES_ACTIVITY);

        // The activity is relaunched again.
        assertRelaunchOrConfigChanged(DESK_RESOURCES_ACTIVITY, 1 /* numRelaunch */,
                0 /* numConfigChange */);
    }

    boolean getConfigSkipRelaunchOnDock() {
        return mContext.getResources().getBoolean(
                Resources.getSystem().getIdentifier("config_skipActivityRelaunchWhenDocking",
                        "bool", "android"));
    }

    /**
     * Rotates the device to the same rotation as it would rotate to when docked.
     *
     * Dock rotation is read from config_deskDockRotation.
     */
    void rotateToDockRotation(RotationSession rotationSession) {
        int rotation = rotationDegreesToConst(mContext.getResources().getInteger(
                Resources.getSystem().getIdentifier("config_deskDockRotation",
                        "integer", "android")));
        if (rotation == -1) {
            // -1 could come from the const itself, which means no rotation on dock, or from
            // rotationDegreesToConst, which means we got an unexpected value from the resource.
            return;
        }
        rotationSession.set(rotation);
    }

    /**
     * Converts from a rotation in degrees to a {@link Surface.Rotation} constant.
     *
     * Returns -1 if a value that doesn't match a {@link Surface.Rotation} constant is provided.
     */
    private int rotationDegreesToConst(int rotationDegrees) {
        switch (rotationDegrees) {
            case 0:
                return Surface.ROTATION_0;
            case 90:
                return Surface.ROTATION_90;
            case 180:
                return Surface.ROTATION_180;
            case 270:
                return Surface.ROTATION_270;
        }
        return -1;
    }


    private class DockTestSession implements AutoCloseable {
        private void dock() {
            runShellCommand("dumpsys DockObserver set state "
                    + Intent.EXTRA_DOCK_STATE_HE_DESK);
        }

        private void undock() {
            runShellCommand("dumpsys DockObserver set state "
                    + Intent.EXTRA_DOCK_STATE_UNDOCKED);
        }

        private void waitForDeskUiMode(ComponentName activity) {
            waitForUiMode(activity, UI_MODE_TYPE_DESK);
        }

        private void waitForNormalUiMode(ComponentName activity) {
            waitForUiMode(activity, UI_MODE_TYPE_NORMAL);
        }

        /**
         * Waits until the activity has received the expected uiMode in a configuration change.
         */
        private void waitForUiMode(ComponentName activity, int expectedUiMode) {
            mWmState.waitFor(state -> {
                int actualUiMode = state.getActivity(activity).getUiMode();
                return (actualUiMode & UI_MODE_TYPE_MASK) == actualUiMode;
            }, "Didn't enter expected uiMode in time: " + expectedUiMode);
        }

        @Override
        public void close() throws Exception {
            runShellCommand("dumpsys DockObserver reset");
        }
    }
}
