/*
 * Copyright (C) 2018 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.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;
import static android.view.KeyEvent.ACTION_DOWN;
import static android.view.KeyEvent.ACTION_UP;
import static android.view.KeyEvent.FLAG_CANCELED;
import static android.view.KeyEvent.KEYCODE_0;
import static android.view.KeyEvent.KEYCODE_1;
import static android.view.KeyEvent.KEYCODE_2;
import static android.view.KeyEvent.KEYCODE_3;
import static android.view.KeyEvent.KEYCODE_4;
import static android.view.KeyEvent.KEYCODE_5;
import static android.view.KeyEvent.KEYCODE_6;
import static android.view.KeyEvent.KEYCODE_7;
import static android.view.KeyEvent.KEYCODE_8;
import static android.view.KeyEvent.keyCodeToString;

import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;

import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Canvas;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.media.ImageReader;
import android.os.SystemClock;
import android.platform.test.annotations.Presubmit;
import android.view.Display;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager.LayoutParams;

import androidx.annotation.NonNull;

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

import org.junit.Test;

import java.util.ArrayList;

import javax.annotation.concurrent.GuardedBy;

/**
 * Ensure window focus assignment is executed as expected.
 *
 * Build/Install/Run:
 *     atest WindowFocusTests
 */
@Presubmit
public class WindowFocusTests extends WindowManagerTestBase {

    private static void sendKey(int action, int keyCode, int displayId) {
        final KeyEvent keyEvent = new KeyEvent(action, keyCode);
        keyEvent.setDisplayId(displayId);
        SystemUtil.runWithShellPermissionIdentity(() -> {
            getInstrumentation().sendKeySync(keyEvent);
        });
    }

    private static void sendAndAssertTargetConsumedKey(InputTargetActivity target, int keyCode,
            int targetDisplayId) {
        sendAndAssertTargetConsumedKey(target, ACTION_DOWN, keyCode, targetDisplayId);
        sendAndAssertTargetConsumedKey(target, ACTION_UP, keyCode, targetDisplayId);
    }

    private static void sendAndAssertTargetConsumedKey(InputTargetActivity target, int action,
            int keyCode, int targetDisplayId) {
        final int eventCount = target.getKeyEventCount();
        sendKey(action, keyCode, targetDisplayId);
        target.assertAndConsumeKeyEvent(action, keyCode, 0 /* flags */);
        assertEquals(target.getLogTag() + " must only receive key event sent.", eventCount,
                target.getKeyEventCount());
    }

    private static void tapOn(@NonNull Activity activity) {
        final Point p = getCenterOfActivityOnScreen(activity);
        final int displayId = activity.getDisplayId();

        final long downTime = SystemClock.elapsedRealtime();
        final MotionEvent downEvent = MotionEvent.obtain(downTime, downTime,
                MotionEvent.ACTION_DOWN, p.x, p.y, 0 /* metaState */);
        downEvent.setDisplayId(displayId);
        getInstrumentation().sendPointerSync(downEvent);
        final MotionEvent upEvent = MotionEvent.obtain(downTime, SystemClock.elapsedRealtime(),
                MotionEvent.ACTION_UP, p.x, p.y, 0 /* metaState */);
        upEvent.setDisplayId(displayId);
        getInstrumentation().sendPointerSync(upEvent);
    }

    private static Point getCenterOfActivityOnScreen(@NonNull Activity activity) {
        final View decorView = activity.getWindow().getDecorView();
        final int[] location = new int[2];
        decorView.getLocationOnScreen(location);
        return new Point(location[0] + decorView.getWidth() / 2,
                location[1] + decorView.getHeight() / 2);
    }

    /**
     * Test the following conditions:
     * - Each display can have a focused window at the same time.
     * - Focused windows can receive display-specified key events.
     * - The top focused window can receive display-unspecified key events.
     * - Taping on a display will make the focused window on it become top-focused.
     * - The window which lost top-focus can receive display-unspecified cancel events.
     */
    @Test
    public void testKeyReceiving() {
        final PrimaryActivity primaryActivity = startActivity(PrimaryActivity.class,
                DEFAULT_DISPLAY);
        sendAndAssertTargetConsumedKey(primaryActivity, KEYCODE_0, INVALID_DISPLAY);
        sendAndAssertTargetConsumedKey(primaryActivity, KEYCODE_1, DEFAULT_DISPLAY);

        assumeTrue(supportsMultiDisplay());

        // VirtualDisplay can't maintain perDisplayFocus because it is not trusted,
        // so uses SimulatedDisplay instead.
        final SimulatedDisplaySession session = createManagedSimulatedDisplaySession();
        final int secondaryDisplayId = session.getDisplayId();
        final SecondaryActivity secondaryActivity = session.startActivityAndFocus();
        sendAndAssertTargetConsumedKey(secondaryActivity, KEYCODE_2, INVALID_DISPLAY);
        sendAndAssertTargetConsumedKey(secondaryActivity, KEYCODE_3, secondaryDisplayId);

        final boolean perDisplayFocusEnabled = perDisplayFocusEnabled();
        if (perDisplayFocusEnabled) {
            primaryActivity.assertWindowFocusState(true /* hasFocus */);
            sendAndAssertTargetConsumedKey(primaryActivity, KEYCODE_4, DEFAULT_DISPLAY);
        } else {
            primaryActivity.waitAndAssertWindowFocusState(false /* hasFocus */);
        }

        // Press display-unspecified keys and a display-specified key but not release them.
        sendKey(ACTION_DOWN, KEYCODE_5, INVALID_DISPLAY);
        sendKey(ACTION_DOWN, KEYCODE_6, secondaryDisplayId);
        sendKey(ACTION_DOWN, KEYCODE_7, INVALID_DISPLAY);
        secondaryActivity.assertAndConsumeKeyEvent(ACTION_DOWN, KEYCODE_5, 0 /* flags */);
        secondaryActivity.assertAndConsumeKeyEvent(ACTION_DOWN, KEYCODE_6, 0 /* flags */);
        secondaryActivity.assertAndConsumeKeyEvent(ACTION_DOWN, KEYCODE_7, 0 /* flags */);

        tapOn(primaryActivity);

        // Assert only display-unspecified key would be cancelled after secondary activity is
        // not top focused if per-display focus is enabled. Otherwise, assert all non-released
        // key events sent to secondary activity would be cancelled.
        secondaryActivity.waitAssertAndConsumeKeyEvent(ACTION_UP, KEYCODE_5, FLAG_CANCELED);
        secondaryActivity.waitAssertAndConsumeKeyEvent(ACTION_UP, KEYCODE_7, FLAG_CANCELED);
        if (!perDisplayFocusEnabled) {
            secondaryActivity.waitAssertAndConsumeKeyEvent(ACTION_UP, KEYCODE_6, FLAG_CANCELED);
        }
        assertEquals(secondaryActivity.getLogTag() + " must only receive expected events.",
                0 /* expected event count */, secondaryActivity.getKeyEventCount());

        // Assert primary activity become top focused after tapping on default display.
        sendAndAssertTargetConsumedKey(primaryActivity, KEYCODE_8, INVALID_DISPLAY);
    }

    /**
     * Test if a display targeted by a key event can be moved to top in a single-focus system.
     */
    @Test
    public void testMovingDisplayToTopByKeyEvent() {
        assumeTrue(supportsMultiDisplay());
        assumeFalse(perDisplayFocusEnabled());

        final PrimaryActivity primaryActivity = startActivity(PrimaryActivity.class,
                DEFAULT_DISPLAY);
        final InvisibleVirtualDisplaySession session = createManagedInvisibleDisplaySession();
        final int secondaryDisplayId = session.getDisplayId();
        final SecondaryActivity secondaryActivity = session.startActivityAndFocus();

        sendAndAssertTargetConsumedKey(primaryActivity, KEYCODE_0, DEFAULT_DISPLAY);
        sendAndAssertTargetConsumedKey(primaryActivity, KEYCODE_1, INVALID_DISPLAY);

        sendAndAssertTargetConsumedKey(secondaryActivity, KEYCODE_2, secondaryDisplayId);
        sendAndAssertTargetConsumedKey(secondaryActivity, KEYCODE_3, INVALID_DISPLAY);
    }

    /**
     * Test if the client is notified about window-focus lost after the new focused window is drawn.
     */
    @Test
    public void testDelayLosingFocus() {
        final LosingFocusActivity activity = startActivity(LosingFocusActivity.class,
                DEFAULT_DISPLAY);

        getInstrumentation().runOnMainSync(activity::addChildWindow);
        activity.waitAndAssertWindowFocusState(false /* hasFocus */);
        assertFalse("Activity must lose window focus after new focused window is drawn.",
                activity.losesFocusWhenNewFocusIsNotDrawn());
    }


    /**
     * Test the following conditions:
     * - Only the top focused window can have pointer capture.
     * - The window which lost top-focus can be notified about pointer-capture lost.
     */
    @Test
    public void testPointerCapture() {
        final PrimaryActivity primaryActivity = startActivity(PrimaryActivity.class,
                DEFAULT_DISPLAY);

        // Assert primary activity can have pointer capture before we have multiple focused windows.
        getInstrumentation().runOnMainSync(primaryActivity::requestPointerCapture);
        primaryActivity.waitAndAssertPointerCaptureState(true /* hasCapture */);

        assumeTrue(supportsMultiDisplay());
        final SecondaryActivity secondaryActivity =
                createManagedInvisibleDisplaySession().startActivityAndFocus();

        // Assert primary activity lost pointer capture when it is not top focused.
        primaryActivity.waitAndAssertPointerCaptureState(false /* hasCapture */);

        // Assert secondary activity can have pointer capture when it is top focused.
        getInstrumentation().runOnMainSync(secondaryActivity::requestPointerCapture);
        secondaryActivity.waitAndAssertPointerCaptureState(true /* hasCapture */);

        tapOn(primaryActivity);
        primaryActivity.waitAndAssertWindowFocusState(true);

        // Assert secondary activity lost pointer capture when it is not top focused.
        secondaryActivity.waitAndAssertPointerCaptureState(false /* hasCapture */);
    }

    /**
     * Pointer capture could be requested after activity regains focus.
     */
    @Test
    public void testPointerCaptureWhenFocus() {
        final AutoEngagePointerCaptureActivity primaryActivity =
                startActivity(AutoEngagePointerCaptureActivity.class, DEFAULT_DISPLAY);

        // Assert primary activity can have pointer capture before we have multiple focused windows.
        primaryActivity.waitAndAssertPointerCaptureState(true /* hasCapture */);

        assumeTrue(supportsMultiDisplay());

        // This test only makes sense if `config_perDisplayFocusEnabled` is disabled.
        assumeFalse(perDisplayFocusEnabled());

        final SecondaryActivity secondaryActivity =
                createManagedInvisibleDisplaySession().startActivityAndFocus();

        primaryActivity.waitAndAssertWindowFocusState(false /* hasFocus */);
        // Assert primary activity lost pointer capture when it is not top focused.
        primaryActivity.waitAndAssertPointerCaptureState(false /* hasCapture */);
        secondaryActivity.waitAndAssertPointerCaptureState(false /* hasCapture */);

        tapOn(primaryActivity);
        primaryActivity.waitAndAssertWindowFocusState(true /* hasFocus */);
        primaryActivity.waitAndAssertPointerCaptureState(true /* hasCapture */);
    }

    /**
     * Test if the focused window can still have focus after it is moved to another display.
     */
    @Test
    public void testDisplayChanged() {
        assumeTrue(supportsMultiDisplay());

        final PrimaryActivity primaryActivity = startActivity(PrimaryActivity.class,
                DEFAULT_DISPLAY);

        final InvisibleVirtualDisplaySession session = createManagedInvisibleDisplaySession();
        final int secondaryDisplayId = session.getDisplayId();
        final SecondaryActivity secondaryActivity = session.startActivityAndFocus();
        // Secondary display disconnected.
        session.close();

        assertNotNull("SecondaryActivity must be started.", secondaryActivity);
        secondaryActivity.waitAndAssertDisplayId(DEFAULT_DISPLAY);
        secondaryActivity.waitAndAssertWindowFocusState(true /* hasFocus */);

        primaryActivity.waitAndAssertWindowFocusState(false /* hasFocus */);
    }

    /**
     * Ensure that a non focused display becomes focused when tapping on a focusable window on
     * that display.
     */
    @Test
    public void testTapFocusableWindow() {
        assumeTrue(supportsMultiDisplay());
        assumeFalse(perDisplayFocusEnabled());

        PrimaryActivity primaryActivity = startActivity(PrimaryActivity.class, DEFAULT_DISPLAY);
        final SecondaryActivity secondaryActivity =
                createManagedInvisibleDisplaySession().startActivityAndFocus();

        tapOn(primaryActivity);
        // Ensure primary activity got focus
        primaryActivity.waitAndAssertWindowFocusState(true);
        secondaryActivity.waitAndAssertWindowFocusState(false);
    }

    /**
     * Ensure that a non focused display does not become focused when tapping on a non-focusable
     * window on that display.
     */
    @Test
    public void testTapNonFocusableWindow() {
        assumeTrue(supportsMultiDisplay());
        assumeFalse(perDisplayFocusEnabled());

        PrimaryActivity primaryActivity = startActivity(PrimaryActivity.class, DEFAULT_DISPLAY);
        final SecondaryActivity secondaryActivity =
                createManagedInvisibleDisplaySession().startActivityAndFocus();

        // Tap on a window that can't be focused and ensure that the other window in that
        // display, primaryActivity's window, doesn't get focus.
        getInstrumentation().runOnMainSync(() -> {
            View view = new View(primaryActivity);
            LayoutParams p = new LayoutParams();
            p.flags = LayoutParams.FLAG_NOT_FOCUSABLE;
            primaryActivity.getWindowManager().addView(view, p);
        });
        getInstrumentation().waitForIdleSync();

        tapOn(primaryActivity);
        // Ensure secondary activity still has focus
        secondaryActivity.waitAndAssertWindowFocusState(true);
        primaryActivity.waitAndAssertWindowFocusState(false);
    }

    private static class InputTargetActivity extends FocusableActivity {
        private static final long TIMEOUT_DISPLAY_CHANGED = 5000; // milliseconds
        private static final long TIMEOUT_POINTER_CAPTURE_CHANGED = 1000;
        private static final long TIMEOUT_NEXT_KEY_EVENT = 1000;

        private final Object mLockPointerCapture = new Object();
        private final Object mLockKeyEvent = new Object();

        @GuardedBy("this")
        private int mDisplayId = INVALID_DISPLAY;
        @GuardedBy("mLockPointerCapture")
        private boolean mHasPointerCapture;
        @GuardedBy("mLockKeyEvent")
        private ArrayList<KeyEvent> mKeyEventList = new ArrayList<>();

        @Override
        public void onAttachedToWindow() {
            synchronized (this) {
                mDisplayId = getWindow().getDecorView().getDisplay().getDisplayId();
                notify();
            }
        }

        @Override
        public void onMovedToDisplay(int displayId, Configuration config) {
            synchronized (this) {
                mDisplayId = displayId;
                notify();
            }
        }

        void waitAndAssertDisplayId(int displayId) {
            synchronized (this) {
                if (mDisplayId != displayId) {
                    try {
                        wait(TIMEOUT_DISPLAY_CHANGED);
                    } catch (InterruptedException e) {
                    }
                }
                assertEquals(getLogTag() + " must be moved to the display.",
                        displayId, mDisplayId);
            }
        }

        @Override
        public void onPointerCaptureChanged(boolean hasCapture) {
            synchronized (mLockPointerCapture) {
                mHasPointerCapture = hasCapture;
                mLockPointerCapture.notify();
            }
        }

        void waitAndAssertPointerCaptureState(boolean hasCapture) {
            synchronized (mLockPointerCapture) {
                if (mHasPointerCapture != hasCapture) {
                    try {
                        mLockPointerCapture.wait(TIMEOUT_POINTER_CAPTURE_CHANGED);
                    } catch (InterruptedException e) {
                    }
                }
                assertEquals(getLogTag() + " must" + (hasCapture ? "" : " not")
                        + " have pointer capture.", hasCapture, mHasPointerCapture);
            }
        }

        // Should be only called from the main thread.
        void requestPointerCapture() {
            getWindow().getDecorView().requestPointerCapture();
        }

        @Override
        public boolean dispatchKeyEvent(KeyEvent event) {
            synchronized (mLockKeyEvent) {
                mKeyEventList.add(event);
                mLockKeyEvent.notify();
            }
            return true;
        }

        int getKeyEventCount() {
            synchronized (mLockKeyEvent) {
                return mKeyEventList.size();
            }
        }

        private KeyEvent consumeKeyEvent(int action, int keyCode, int flags) {
            synchronized (mLockKeyEvent) {
                for (int i = mKeyEventList.size() - 1; i >= 0; i--) {
                    final KeyEvent event = mKeyEventList.get(i);
                    if (event.getAction() == action && event.getKeyCode() == keyCode
                            && (event.getFlags() & flags) == flags) {
                        mKeyEventList.remove(event);
                        return event;
                    }
                }
            }
            return null;
        }

        void assertAndConsumeKeyEvent(int action, int keyCode, int flags) {
            assertNotNull(getLogTag() + " must receive key event " + keyCodeToString(keyCode),
                    consumeKeyEvent(action, keyCode, flags));
        }

        void waitAssertAndConsumeKeyEvent(int action, int keyCode, int flags) {
            if (consumeKeyEvent(action, keyCode, flags) == null) {
                synchronized (mLockKeyEvent) {
                    try {
                        mLockKeyEvent.wait(TIMEOUT_NEXT_KEY_EVENT);
                    } catch (InterruptedException e) {
                    }
                }
                assertAndConsumeKeyEvent(action, keyCode, flags);
            }
        }
    }

    public static class PrimaryActivity extends InputTargetActivity { }

    public static class SecondaryActivity extends InputTargetActivity { }

    public static class LosingFocusActivity extends InputTargetActivity {
        private boolean mChildWindowHasDrawn = false;

        @GuardedBy("this")
        private boolean mLosesFocusWhenNewFocusIsNotDrawn = false;

        void addChildWindow() {
            getWindowManager().addView(new View(this) {
                @Override
                protected void onDraw(Canvas canvas) {
                    mChildWindowHasDrawn = true;
                }
            }, new LayoutParams());
        }

        @Override
        public void onWindowFocusChanged(boolean hasFocus) {
            if (!hasFocus && !mChildWindowHasDrawn) {
                synchronized (this) {
                    mLosesFocusWhenNewFocusIsNotDrawn = true;
                }
            }
            super.onWindowFocusChanged(hasFocus);
        }

        boolean losesFocusWhenNewFocusIsNotDrawn() {
            synchronized (this) {
                return mLosesFocusWhenNewFocusIsNotDrawn;
            }
        }
    }

    public static class AutoEngagePointerCaptureActivity extends InputTargetActivity {
        @Override
        public void onWindowFocusChanged(boolean hasFocus) {
            if (hasFocus) {
                requestPointerCapture();
            }
            super.onWindowFocusChanged(hasFocus);
        }
    }

    private InvisibleVirtualDisplaySession createManagedInvisibleDisplaySession() {
        return mObjectTracker.manage(
                new InvisibleVirtualDisplaySession(getInstrumentation().getTargetContext()));
    }

    /** An untrusted virtual display that won't show on default screen. */
    private static class InvisibleVirtualDisplaySession implements AutoCloseable {
        private static final int WIDTH = 800;
        private static final int HEIGHT = 480;
        private static final int DENSITY = 160;

        private final VirtualDisplay mVirtualDisplay;
        private final ImageReader mReader;
        private final Display mDisplay;

        InvisibleVirtualDisplaySession(Context context) {
            mReader = ImageReader.newInstance(WIDTH, HEIGHT, PixelFormat.RGBA_8888,
                    2 /* maxImages */);
            mVirtualDisplay = context.getSystemService(DisplayManager.class)
                    .createVirtualDisplay(WindowFocusTests.class.getSimpleName(),
                            WIDTH, HEIGHT, DENSITY, mReader.getSurface(),
                            VIRTUAL_DISPLAY_FLAG_PUBLIC | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY);
            mDisplay = mVirtualDisplay.getDisplay();
        }

        int getDisplayId() {
            return mDisplay.getDisplayId();
        }

        SecondaryActivity startActivityAndFocus() {
            return WindowFocusTests.startActivityAndFocus(getDisplayId(), false /* hasFocus */);
        }

        @Override
        public void close() {
            if (mVirtualDisplay != null) {
                mVirtualDisplay.release();
            }
            if (mReader != null) {
                mReader.close();
            }
        }
    }

    private SimulatedDisplaySession createManagedSimulatedDisplaySession() {
        return mObjectTracker.manage(new SimulatedDisplaySession());
    }

    private class SimulatedDisplaySession implements AutoCloseable {
        private final VirtualDisplaySession mVirtualDisplaySession;
        private final WindowManagerState.DisplayContent mVirtualDisplay;

        SimulatedDisplaySession() {
            mVirtualDisplaySession = new VirtualDisplaySession();
            mVirtualDisplay = mVirtualDisplaySession.setSimulateDisplay(true).createDisplay();
        }

        int getDisplayId() {
            return mVirtualDisplay.mId;
        }

        SecondaryActivity startActivityAndFocus() {
            return WindowFocusTests.startActivityAndFocus(getDisplayId(), true /* hasFocus */);
        }

        @Override
        public void close() {
            mVirtualDisplaySession.close();
        }
    }

    private static SecondaryActivity startActivityAndFocus(int displayId, boolean hasFocus) {
        // An untrusted virtual display won't have focus until the display is touched.
        final SecondaryActivity activity = WindowManagerTestBase.startActivity(
                SecondaryActivity.class, displayId, hasFocus);
        tapOn(activity);
        activity.waitAndAssertWindowFocusState(true);
        return activity;
    }
}
