/*
 * Copyright (C) 2020 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.globalactions;

import static android.provider.Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD;
import static android.view.WindowInsets.Type.ime;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import android.app.Activity;
import android.content.ContentResolver;
import android.os.Bundle;
import android.os.PowerManager;
import android.os.SystemClock;
import android.provider.Settings;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
import android.widget.EditText;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.filters.FlakyTest;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;

import com.android.systemui.SysuiTestCase;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.function.BooleanSupplier;

@LargeTest
@FlakyTest(bugId = 176891566)
public class GlobalActionsImeTest extends SysuiTestCase {

    @Rule
    public ActivityTestRule<TestActivity> mActivityTestRule = new ActivityTestRule<>(
            TestActivity.class, false, false);

    private int mOriginalShowImeWithHardKeyboard;

    @Before
    public void setUp() {
        final ContentResolver contentResolver = mContext.getContentResolver();
        mOriginalShowImeWithHardKeyboard = Settings.Secure.getInt(
                contentResolver, SHOW_IME_WITH_HARD_KEYBOARD, 0);
        // Forcibly shows IME even when hardware keyboard is connected.
        // To change USER_SYSTEM settings, we have to use settings shell command.
        executeShellCommand("settings put secure " + SHOW_IME_WITH_HARD_KEYBOARD + " 1");
    }

    @After
    public void tearDown() {
        // To restore USER_SYSTEM settings, we have to use settings shell command.
        executeShellCommand("settings put secure "
                + SHOW_IME_WITH_HARD_KEYBOARD + " " + mOriginalShowImeWithHardKeyboard);
        // Hide power menu and return to home screen
        executeShellCommand("input keyevent --longpress POWER");
        executeShellCommand("input keyevent HOME");
    }

    /**
     * This test verifies that GlobalActions, which is frequently used to capture bugreports,
     * doesn't interfere with the IME, i.e. soft-keyboard state.
     */
    @Test
    public void testGlobalActions_doesntStealImeControl() throws Exception {
        turnScreenOn();
        final TestActivity activity = mActivityTestRule.launchActivity(null);
        boolean isImeVisible = waitUntil(activity::isImeVisible);
        if (!isImeVisible) {
            // Sometimes the keyboard is dismissed when run with other tests. Bringing it up again
            // should improve test reliability
            activity.showIme();
            waitUntil("Ime is not visible", activity::isImeVisible);
        }

        // In some cases, IME is not controllable. e.g., floating IME or fullscreen IME.
        final boolean activityControlledIme = activity.mControlsIme;

        executeShellCommand("input keyevent --longpress POWER");

        waitUntil("activity loses focus", () -> !activity.mHasFocus);
        // Give the dialog time to animate in, and steal IME focus. Unfortunately, there's currently
        // no better way to wait for this.
        SystemClock.sleep(TimeUnit.SECONDS.toMillis(2));

        runAssertionOnMainThread(() -> {
            assertTrue("IME should remain visible behind GlobalActions, but didn't",
                    activity.mImeVisible);
            assertEquals("App behind GlobalActions should remain in control of IME, but didn't",
                    activityControlledIme, activity.mControlsIme);
        });
    }

    private void turnScreenOn() throws Exception {
        PowerManager powerManager = mContext.getSystemService(PowerManager.class);
        assertNotNull(powerManager);
        if (powerManager.isInteractive()) {
            return;
        }
        executeShellCommand("input keyevent KEYCODE_WAKEUP");
        waitUntil("Device not interactive", powerManager::isInteractive);
        executeShellCommand("am wait-for-broadcast-idle");
    }

    private static void waitUntil(String message, BooleanSupplier predicate)
            throws Exception {
        if (!waitUntil(predicate)) {
            fail(message);
        }
    }

    private static boolean waitUntil(BooleanSupplier predicate) throws Exception {
        int sleep = 125;
        final long timeout = SystemClock.uptimeMillis() + 10_000;  // 10 second timeout
        while (SystemClock.uptimeMillis() < timeout) {
            if (predicate.getAsBoolean()) {
                return true;
            }
            Thread.sleep(sleep);
            sleep *= 5;
            sleep = Math.min(2000, sleep);
        }
        return false;
    }

    private void executeShellCommand(String cmd) {
        try {
            runShellCommand(cmd);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Like Instrumentation.runOnMainThread(), but forwards AssertionErrors to the caller.
     */
    private static void runAssertionOnMainThread(Runnable r) {
        AssertionError[] t = new AssertionError[1];
        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
            try {
                r.run();
            } catch (AssertionError e) {
                t[0] = e;
                // Ignore assertion - throwing it here would crash the main thread.
            }
        });
        if (t[0] != null) {
            throw t[0];
        }
    }

    public static class TestActivity extends Activity implements
            WindowInsetsController.OnControllableInsetsChangedListener,
            View.OnApplyWindowInsetsListener {

        private EditText mEditText;
        boolean mHasFocus;
        boolean mControlsIme;
        boolean mImeVisible;

        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setShowWhenLocked(true); // Allow this test to work even if device got stuck on keyguard
            mEditText = new EditText(this);
            mEditText.setCursorVisible(false);  // Otherwise, main thread doesn't go idle.
            setContentView(mEditText);
            showIme();
        }

        private void showIme() {
            mEditText.requestFocus();
            getWindow().getDecorView().setOnApplyWindowInsetsListener(this);
            WindowInsetsController wic = mEditText.getWindowInsetsController();
            wic.addOnControllableInsetsChangedListener(this);
            wic.show(ime());
        }

        @Override
        public void onWindowFocusChanged(boolean hasFocus) {
            synchronized (this) {
                mHasFocus = hasFocus;
                notifyAll();
            }
        }

        @Override
        public void onControllableInsetsChanged(@NonNull WindowInsetsController controller,
                int typeMask) {
            synchronized (this) {
                mControlsIme = (typeMask & ime()) != 0;
                notifyAll();
            }
        }

        boolean isImeVisible() {
            return mHasFocus && mImeVisible;
        }

        @Override
        public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
            mImeVisible = insets.isVisible(ime());
            return v.onApplyWindowInsets(insets);
        }
    }
}
