/*
 * 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 android.server.wm;

import static android.server.wm.ActivityManagerTestBase.launchHomeActivityNoWait;
import static android.server.wm.BarTestUtils.assumeHasStatusBar;
import static android.server.wm.UiDeviceUtils.pressUnlockButton;
import static android.server.wm.UiDeviceUtils.pressWakeupButton;
import static android.server.wm.WindowUntrustedTouchTest.MIN_POSITIVE_OPACITY;
import static android.server.wm.app.Components.OverlayTestService.EXTRA_LAYOUT_PARAMS;
import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;

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

import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;

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

import android.app.Activity;
import android.app.Instrumentation;
import android.content.ContentResolver;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.input.InputManager;
import android.os.Bundle;
import android.os.SystemClock;
import android.platform.test.annotations.Presubmit;
import android.provider.Settings;
import android.server.wm.WindowManagerState.WindowState;
import android.server.wm.app.Components;
import android.server.wm.settings.SettingsSession;
import android.util.ArraySet;
import android.util.Log;
import android.view.Gravity;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;

import androidx.test.filters.FlakyTest;
import androidx.test.rule.ActivityTestRule;

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

import org.junit.Before;
import org.junit.Test;

import java.util.ArrayList;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeoutException;

/**
 * Ensure moving windows and tapping is done synchronously.
 *
 * Build/Install/Run:
 *     atest CtsWindowManagerDeviceTestCases:WindowInputTests
 */
@Presubmit
public class WindowInputTests {
    private static final String TAG = "WindowInputTests";
    private final int TOTAL_NUMBER_OF_CLICKS = 100;
    private final ActivityTestRule<TestActivity> mActivityRule =
            new ActivityTestRule<>(TestActivity.class);
    private static final int TAPPING_TARGET_WINDOW_SIZE = 100;
    private static final int PARTIAL_OBSCURING_WINDOW_SIZE = 30;

    private Instrumentation mInstrumentation;
    private final WindowManagerStateHelper mWmState = new WindowManagerStateHelper();
    private TestActivity mActivity;
    private InputManager mInputManager;
    private View mView;
    private final Random mRandom = new Random(1);

    private int mClickCount = 0;
    private final long EVENT_FLAGS_WAIT_TIME = 2;

    @Before
    public void setUp() {
        pressWakeupButton();
        pressUnlockButton();
        launchHomeActivityNoWait();

        mInstrumentation = getInstrumentation();
        mActivity = mActivityRule.launchActivity(null);
        mInputManager = mActivity.getSystemService(InputManager.class);
        mInstrumentation.waitForIdleSync();
        mClickCount = 0;
    }

    @Test
    @FlakyTest(bugId = 188207199)
    public void testMoveWindowAndTap() throws Throwable {
        final WindowManager wm = mActivity.getWindowManager();
        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
        p.setFitInsetsTypes(WindowInsets.Type.systemBars()
                | WindowInsets.Type.systemGestures());
        p.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        p.width = p.height = 20;
        p.gravity = Gravity.LEFT | Gravity.TOP;

        // Set up window.
        mActivityRule.runOnUiThread(() -> {
            mView = new View(mActivity);
            mView.setBackgroundColor(Color.RED);
            mView.setOnClickListener((v) -> {
                mClickCount++;
            });
            mActivity.addWindow(mView, p);
        });
        mInstrumentation.waitForIdleSync();

        final WindowMetrics windowMetrics = wm.getCurrentWindowMetrics();
        final WindowInsets windowInsets = windowMetrics.getWindowInsets();
        final Rect windowBounds = new Rect(windowMetrics.getBounds());
        windowBounds.inset(windowInsets.getInsetsIgnoringVisibility(p.getFitInsetsTypes()));

        // Move the window to a random location in the window and attempt to tap on view multiple
        // times.
        final Point locationInWindow = new Point();
        for (int i = 0; i < TOTAL_NUMBER_OF_CLICKS; i++) {
            selectRandomLocationInWindow(windowBounds, locationInWindow);
            mActivityRule.runOnUiThread(() -> {
                p.x = locationInWindow.x;
                p.y = locationInWindow.y;
                wm.updateViewLayout(mView, p);
            });
            mInstrumentation.waitForIdleSync();
            int previousCount = mClickCount;

            CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView);

            mInstrumentation.waitForIdleSync();
            if (mClickCount != previousCount + 1) {
                final int vW = mView.getWidth();
                final int vH = mView.getHeight();
                final int[] viewOnScreenXY = new int[2];
                mView.getLocationOnScreen(viewOnScreenXY);
                final Point tapPosition =
                        new Point(viewOnScreenXY[0] + vW / 2, viewOnScreenXY[1] + vH / 2);
                final Rect realBounds = new Rect(viewOnScreenXY[0], viewOnScreenXY[1],
                        viewOnScreenXY[0] + vW, viewOnScreenXY[1] + vH);
                final Rect requestedBounds = new Rect(p.x, p.y, p.x + p.width, p.y + p.height);
                dumpWindows("Dumping windows due to failure");
                fail("Tap #" + i + " on " + tapPosition + " failed; realBounds=" + realBounds
                        + " requestedBounds=" + requestedBounds);
            }
        }

        assertEquals(TOTAL_NUMBER_OF_CLICKS, mClickCount);
    }

    private void dumpWindows(String message) {
        Log.d(TAG, message);
        mWmState.computeState();
        for (WindowState window : mWmState.getWindows()) {
            Log.d(TAG, "    => " + window.toLongString());
        }
    }

    private void selectRandomLocationInWindow(Rect bounds, Point outLocation) {
        int randomX = mRandom.nextInt(bounds.right - bounds.left) + bounds.left;
        int randomY = mRandom.nextInt(bounds.bottom - bounds.top) + bounds.top;
        outLocation.set(randomX, randomY);
    }

    @Test
    public void testTouchModalWindow() throws Throwable {
        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();

        // Set up 2 touch modal windows, expect the last one will receive all touch events.
        mActivityRule.runOnUiThread(() -> {
            mView = new View(mActivity);
            p.width = 20;
            p.height = 20;
            p.gravity = Gravity.LEFT | Gravity.CENTER_VERTICAL;
            mView.setFilterTouchesWhenObscured(true);
            mView.setOnClickListener((v) -> {
                mClickCount++;
            });
            mActivity.addWindow(mView, p);

            View view2 = new View(mActivity);
            p.gravity = Gravity.RIGHT | Gravity.CENTER_VERTICAL;
            p.type = WindowManager.LayoutParams.TYPE_APPLICATION;
            mActivity.addWindow(view2, p);
        });
        mInstrumentation.waitForIdleSync();

        CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView);
        assertEquals(0, mClickCount);
    }

    // If a window is obscured by another window from the same app, touches should still get
    // delivered to the bottom window, and the FLAG_WINDOW_IS_OBSCURED should not be set.
    @Test
    public void testFilterTouchesWhenObscuredByWindowFromSameUid() throws Throwable {
        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();

        final AtomicBoolean touchReceived = new AtomicBoolean(false);
        final CompletableFuture<Integer> eventFlags = new CompletableFuture<>();
        // Set up a touchable window.
        mActivityRule.runOnUiThread(() -> {
            mView = new View(mActivity);
            p.flags = FLAG_NOT_TOUCH_MODAL | FLAG_LAYOUT_IN_SCREEN;
            p.width = 100;
            p.height = 100;
            p.gravity = Gravity.CENTER;
            mView.setFilterTouchesWhenObscured(true);
            mView.setOnClickListener((v) -> {
                mClickCount++;
            });
            mView.setOnTouchListener((v, ev) -> {
                touchReceived.set(true);
                eventFlags.complete(ev.getFlags());
                return false;
            });
            mActivity.addWindow(mView, p);

            // Set up an overlap window, use same process.
            View overlay = new View(mActivity);
            p.flags = FLAG_NOT_TOUCH_MODAL | FLAG_LAYOUT_IN_SCREEN | FLAG_NOT_TOUCHABLE;
            p.width = 100;
            p.height = 100;
            p.gravity = Gravity.CENTER;
            p.type = WindowManager.LayoutParams.TYPE_APPLICATION;
            mActivity.addWindow(overlay, p);
        });
        mInstrumentation.waitForIdleSync();
        CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView);

        assertTrue(touchReceived.get());
        assertEquals(0, eventFlags.get(EVENT_FLAGS_WAIT_TIME, TimeUnit.SECONDS)
                & MotionEvent.FLAG_WINDOW_IS_OBSCURED);
        assertEquals(1, mClickCount);
    }

    @Test
    public void testFilterTouchesWhenObscuredByWindowFromDifferentUid() throws Throwable {
        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();

        final Intent intent = new Intent();
        intent.setComponent(Components.OVERLAY_TEST_SERVICE);
        final String windowName = "Test Overlay";
        final AtomicBoolean touchReceived = new AtomicBoolean(false);
        final int[] viewOnScreenLocation = new int[2];
        try {
            // Set up a touchable window.
            mActivityRule.runOnUiThread(() -> {
                mView = new View(mActivity);
                p.flags = FLAG_NOT_TOUCH_MODAL | FLAG_LAYOUT_IN_SCREEN;
                p.width = TAPPING_TARGET_WINDOW_SIZE;
                p.height = TAPPING_TARGET_WINDOW_SIZE;
                p.gravity = Gravity.CENTER;
                mView.setFilterTouchesWhenObscured(true);
                mView.setOnClickListener((v) -> {
                    mClickCount++;
                });
                mView.setOnTouchListener((v, ev) -> {
                    touchReceived.set(true);
                    return false;
                });
                mActivity.addWindow(mView, p);
            });
            mInstrumentation.waitForIdleSync();
            mActivityRule.runOnUiThread(() -> {
                mView.getLocationOnScreen(viewOnScreenLocation);
                // Set up an overlap window from service, use different process.
                WindowManager.LayoutParams params = getObscuringViewLayoutParams(windowName);
                params.flags |= FLAG_NOT_TOUCHABLE;
                placeWindowAtLayoutCenter(params, TAPPING_TARGET_WINDOW_SIZE,
                        viewOnScreenLocation[0], viewOnScreenLocation[1],
                        TAPPING_TARGET_WINDOW_SIZE);
                // Any opacity higher than this would make InputDispatcher block the touch
                params.alpha = mInputManager.getMaximumObscuringOpacityForTouch();
                intent.putExtra(EXTRA_LAYOUT_PARAMS, params);
                mActivity.startForegroundService(intent);
            });
            mInstrumentation.waitForIdleSync();
            waitForWindow(windowName);
            CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView);

            // Touch not received due to setFilterTouchesWhenObscured(true)
            assertFalse(touchReceived.get());
            assertEquals(0, mClickCount);
        } finally {
            mActivity.stopService(intent);
        }
    }

    @Test
    public void testFlagTouchesWhenObscuredByWindowFromDifferentUid() throws Throwable {
        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();

        final Intent intent = new Intent();
        intent.setComponent(Components.OVERLAY_TEST_SERVICE);
        final String windowName = "Test Overlay";
        final AtomicBoolean touchReceived = new AtomicBoolean(false);
        final CompletableFuture<Integer> eventFlags = new CompletableFuture<>();
        final int[] viewOnScreenLocation = new int[2];
        try {
            // Set up a touchable window.
            mActivityRule.runOnUiThread(() -> {
                mView = new View(mActivity);
                p.flags = FLAG_NOT_TOUCH_MODAL | FLAG_LAYOUT_IN_SCREEN;
                p.width = TAPPING_TARGET_WINDOW_SIZE;
                p.height = TAPPING_TARGET_WINDOW_SIZE;
                p.gravity = Gravity.CENTER;
                mView.setOnClickListener((v) -> {
                    mClickCount++;
                });
                mView.setOnTouchListener((v, ev) -> {
                    touchReceived.set(true);
                    eventFlags.complete(ev.getFlags());
                    return false;
                });
                mActivity.addWindow(mView, p);
            });
            mInstrumentation.waitForIdleSync();
            mActivityRule.runOnUiThread(() -> {
                mView.getLocationOnScreen(viewOnScreenLocation);
                // Set up an overlap window from service, use different process.
                WindowManager.LayoutParams params = getObscuringViewLayoutParams(windowName);
                params.flags |= FLAG_NOT_TOUCHABLE;
                // Any opacity higher than this would make InputDispatcher block the touch
                params.alpha = mInputManager.getMaximumObscuringOpacityForTouch();
                placeWindowAtLayoutCenter(params, TAPPING_TARGET_WINDOW_SIZE,
                        viewOnScreenLocation[0], viewOnScreenLocation[1],
                        TAPPING_TARGET_WINDOW_SIZE);
                intent.putExtra(EXTRA_LAYOUT_PARAMS, params);
                mActivity.startForegroundService(intent);
            });
            waitForWindow(windowName);
            CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView);

            assertTrue(touchReceived.get());
            assertEquals(MotionEvent.FLAG_WINDOW_IS_OBSCURED,
                    eventFlags.get(EVENT_FLAGS_WAIT_TIME, TimeUnit.SECONDS)
                            & MotionEvent.FLAG_WINDOW_IS_OBSCURED);
            assertEquals(1, mClickCount);
        } finally {
            mActivity.stopService(intent);
        }
    }

    @Test
    public void testDoNotFlagTouchesWhenObscuredByZeroOpacityWindow() throws Throwable {
        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();

        final Intent intent = new Intent();
        intent.setComponent(Components.OVERLAY_TEST_SERVICE);
        final String windowName = "Test Overlay";
        final AtomicBoolean touchReceived = new AtomicBoolean(false);
        final CompletableFuture<Integer> eventFlags = new CompletableFuture<>();

        try {
            mActivityRule.runOnUiThread(() -> {
                mView = new View(mActivity);
                mView.setBackgroundColor(Color.GREEN);
                p.flags = FLAG_NOT_TOUCH_MODAL | FLAG_LAYOUT_IN_SCREEN;
                p.width = 100;
                p.height = 100;
                p.gravity = Gravity.CENTER;
                mView.setOnClickListener((v) -> {
                    mClickCount++;
                });
                mView.setOnTouchListener((v, ev) -> {
                    touchReceived.set(true);
                    eventFlags.complete(ev.getFlags());
                    return false;
                });
                mActivity.addWindow(mView, p);

                // Set up an overlap window from service, use different process.
                WindowManager.LayoutParams params = getObscuringViewLayoutParams(windowName);
                params.flags |= FLAG_NOT_TOUCHABLE;
                params.alpha = 0;
                intent.putExtra(EXTRA_LAYOUT_PARAMS, params);
                mActivity.startForegroundService(intent);
            });
            mInstrumentation.waitForIdleSync();
            waitForWindow(windowName);
            CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView);

            assertTrue(touchReceived.get());
            assertEquals(0, eventFlags.get(EVENT_FLAGS_WAIT_TIME, TimeUnit.SECONDS)
                    & MotionEvent.FLAG_WINDOW_IS_OBSCURED);
            assertEquals(1, mClickCount);
        } finally {
            mActivity.stopService(intent);
        }
    }

    @Test
    public void testFlagTouchesWhenObscuredByMinPositiveOpacityWindow() throws Throwable {
        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
        final CompletableFuture<Integer> eventFlags = new CompletableFuture<>();
        final Intent intent = new Intent();
        intent.setComponent(Components.OVERLAY_TEST_SERVICE);
        final String windowName = "Test Overlay";
        final AtomicBoolean touchReceived = new AtomicBoolean(false);
        final int[] viewOnScreenLocation = new int[2];
        try {
            mActivityRule.runOnUiThread(() -> {
                mView = new View(mActivity);
                mView.setBackgroundColor(Color.GREEN);
                p.flags = FLAG_NOT_TOUCH_MODAL | FLAG_LAYOUT_IN_SCREEN;
                p.width = TAPPING_TARGET_WINDOW_SIZE;
                p.height = TAPPING_TARGET_WINDOW_SIZE;
                p.gravity = Gravity.CENTER;
                mView.setOnClickListener((v) -> {
                    mClickCount++;
                });
                mView.setOnTouchListener((v, ev) -> {
                    touchReceived.set(true);
                    eventFlags.complete(ev.getFlags());
                    return false;
                });
                mActivity.addWindow(mView, p);
            });
            mInstrumentation.waitForIdleSync();
            mActivityRule.runOnUiThread(() -> {
                mView.getLocationOnScreen(viewOnScreenLocation);
                // Set up an overlap window from service, use different process.
                WindowManager.LayoutParams params = getObscuringViewLayoutParams(windowName);
                params.flags |= FLAG_NOT_TOUCHABLE;
                params.alpha = MIN_POSITIVE_OPACITY;
                placeWindowAtLayoutCenter(params, TAPPING_TARGET_WINDOW_SIZE,
                        viewOnScreenLocation[0], viewOnScreenLocation[1],
                        TAPPING_TARGET_WINDOW_SIZE);
                intent.putExtra(EXTRA_LAYOUT_PARAMS, params);
                mActivity.startForegroundService(intent);
            });
            mInstrumentation.waitForIdleSync();
            waitForWindow(windowName);
            CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView);

            assertTrue(touchReceived.get());
            assertEquals(MotionEvent.FLAG_WINDOW_IS_OBSCURED,
                    eventFlags.get(EVENT_FLAGS_WAIT_TIME, TimeUnit.SECONDS)
                            & MotionEvent.FLAG_WINDOW_IS_OBSCURED);
            assertEquals(1, mClickCount);
        } finally {
            mActivity.stopService(intent);
        }
    }

    @Test
    public void testFlagTouchesWhenPartiallyObscuredByZeroOpacityWindow() throws Throwable {
        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();

        final Intent intent = new Intent();
        intent.setComponent(Components.OVERLAY_TEST_SERVICE);
        final String windowName = "Test Overlay";
        final AtomicBoolean touchReceived = new AtomicBoolean(false);
        final CompletableFuture<Integer> eventFlags = new CompletableFuture<>();
        final int[] viewOnScreenLocation = new int[2];
        try {
            mActivityRule.runOnUiThread(() -> {
                mView = new View(mActivity);
                mView.setBackgroundColor(Color.GREEN);
                p.flags = FLAG_NOT_TOUCH_MODAL | FLAG_LAYOUT_IN_SCREEN;
                p.width = TAPPING_TARGET_WINDOW_SIZE;
                p.height = TAPPING_TARGET_WINDOW_SIZE;
                p.gravity = Gravity.CENTER;
                mView.setOnClickListener((v) -> {
                    mClickCount++;
                });
                mView.setOnTouchListener((v, ev) -> {
                    touchReceived.set(true);
                    eventFlags.complete(ev.getFlags());
                    return false;
                });
                mActivity.addWindow(mView, p);
            });
            mInstrumentation.waitForIdleSync();
            mActivityRule.runOnUiThread(() -> {
                mView.getLocationOnScreen(viewOnScreenLocation);
                // Set up an overlap window from service, use different process.
                WindowManager.LayoutParams params = getObscuringViewLayoutParams(windowName,
                        PARTIAL_OBSCURING_WINDOW_SIZE);
                placeWindowAtLayoutCenter(params, PARTIAL_OBSCURING_WINDOW_SIZE,
                        viewOnScreenLocation[0], viewOnScreenLocation[1], TAPPING_TARGET_WINDOW_SIZE);
                // Move it off the touch path (center) but still overlap with window above
                params.y += PARTIAL_OBSCURING_WINDOW_SIZE;
                intent.putExtra(EXTRA_LAYOUT_PARAMS, params);
                mActivity.startForegroundService(intent);
            });
            mInstrumentation.waitForIdleSync();
            waitForWindow(windowName);
            CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView);

            assertTrue(touchReceived.get());
            assertEquals(MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED,
                    eventFlags.get(EVENT_FLAGS_WAIT_TIME, TimeUnit.SECONDS)
                            & MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED);
            assertEquals(1, mClickCount);
        } finally {
            mActivity.stopService(intent);
        }
    }

    @Test
    public void testDoNotFlagTouchesWhenPartiallyObscuredByNotTouchableZeroOpacityWindow()
            throws Throwable {
        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();

        final Intent intent = new Intent();
        intent.setComponent(Components.OVERLAY_TEST_SERVICE);
        final String windowName = "Test Overlay";
        final AtomicBoolean touchReceived = new AtomicBoolean(false);
        final CompletableFuture<Integer> eventFlags = new CompletableFuture<>();

        try {
            mActivityRule.runOnUiThread(() -> {
                mView = new View(mActivity);
                mView.setBackgroundColor(Color.GREEN);
                p.flags = FLAG_NOT_TOUCH_MODAL | FLAG_LAYOUT_IN_SCREEN;
                p.width = 100;
                p.height = 100;
                p.gravity = Gravity.CENTER;
                mView.setOnClickListener((v) -> {
                    mClickCount++;
                });
                mView.setOnTouchListener((v, ev) -> {
                    touchReceived.set(true);
                    eventFlags.complete(ev.getFlags());
                    return false;
                });
                mActivity.addWindow(mView, p);

                // Set up an overlap window from service, use different process.
                WindowManager.LayoutParams params = getObscuringViewLayoutParams(windowName, 30);
                params.flags |= FLAG_NOT_TOUCHABLE;
                // Move it off the touch path (center) but still overlap with window above
                params.y = 30;
                params.alpha = 0;
                intent.putExtra(EXTRA_LAYOUT_PARAMS, params);
                mActivity.startForegroundService(intent);
            });
            mInstrumentation.waitForIdleSync();
            waitForWindow(windowName);
            CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView);

            assertTrue(touchReceived.get());
            assertEquals(0, eventFlags.get(EVENT_FLAGS_WAIT_TIME, TimeUnit.SECONDS)
                    & MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED);
            assertEquals(1, mClickCount);
        } finally {
            mActivity.stopService(intent);
        }
    }

    private WindowManager.LayoutParams getObscuringViewLayoutParams(String windowName) {
        return getObscuringViewLayoutParams(windowName, 100);
    }

    private WindowManager.LayoutParams getObscuringViewLayoutParams(String windowName, int size) {
        WindowManager.LayoutParams params = new WindowManager.LayoutParams();
        params.setTitle(windowName);
        params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
        params.flags = FLAG_NOT_TOUCH_MODAL | FLAG_LAYOUT_IN_SCREEN;
        params.width = size;
        params.height = size;
        params.gravity = Gravity.CENTER;
        return params;
    }

    @Test
    public void testTrustedOverlapWindow() throws Throwable {
        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
        try (final PointerLocationSession session = new PointerLocationSession()) {
            session.set(true);
            session.waitForReady(mActivity.getDisplayId());

            // Set up window.
            mActivityRule.runOnUiThread(() -> {
                mView = new View(mActivity);
                p.width = 20;
                p.height = 20;
                p.gravity = Gravity.CENTER;
                mView.setFilterTouchesWhenObscured(true);
                mView.setOnClickListener((v) -> {
                    mClickCount++;
                });
                mActivity.addWindow(mView, p);

            });
            mInstrumentation.waitForIdleSync();

            CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView);
        }
        assertEquals(1, mClickCount);
    }

    @Test
    public void testWindowBecomesUnTouchable() throws Throwable {
        final WindowManager wm = mActivity.getWindowManager();
        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();

        final View viewOverlap = new View(mActivity);

        // Set up window.
        mActivityRule.runOnUiThread(() -> {
            mView = new View(mActivity);
            p.width = 20;
            p.height = 20;
            p.gravity = Gravity.CENTER;
            mView.setOnClickListener((v) -> {
                mClickCount++;
            });
            mActivity.addWindow(mView, p);

            p.width = 100;
            p.height = 100;
            p.gravity = Gravity.CENTER;
            p.type = WindowManager.LayoutParams.TYPE_APPLICATION;
            mActivity.addWindow(viewOverlap, p);
        });
        mInstrumentation.waitForIdleSync();

        CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView);
        assertEquals(0, mClickCount);

        mActivityRule.runOnUiThread(() -> {
            p.flags = FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE;
            wm.updateViewLayout(viewOverlap, p);
        });
        mInstrumentation.waitForIdleSync();

        CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView);
        assertEquals(1, mClickCount);
    }

    @Test
    public void testTapInsideUntouchableWindowResultInOutsideTouches() throws Throwable {
        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();

        final Set<MotionEvent> events = new ArraySet<>();
        mActivityRule.runOnUiThread(() -> {
            mView = new View(mActivity);
            p.width = 20;
            p.height = 20;
            p.gravity = Gravity.CENTER;
            p.flags = FLAG_NOT_TOUCHABLE | FLAG_WATCH_OUTSIDE_TOUCH;
            mView.setOnTouchListener((v, e) -> {
                // Copying to make sure we are not dealing with a reused object
                events.add(MotionEvent.obtain(e));
                return false;
            });
            mActivity.addWindow(mView, p);
        });
        mInstrumentation.waitForIdleSync();

        CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mView);

        assertEquals(1, events.size());
        MotionEvent event = events.iterator().next();
        assertEquals(MotionEvent.ACTION_OUTSIDE, event.getAction());
    }

    @Test
    public void testTapOutsideUntouchableWindowResultInOutsideTouches() throws Throwable {
        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();

        Set<MotionEvent> events = new ArraySet<>();
        int size = 20;
        mActivityRule.runOnUiThread(() -> {
            mView = new View(mActivity);
            p.width = size;
            p.height = size;
            p.gravity = Gravity.CENTER;
            p.flags = FLAG_NOT_TOUCHABLE | FLAG_WATCH_OUTSIDE_TOUCH;
            mView.setOnTouchListener((v, e) -> {
                // Copying to make sure we are not dealing with a reused object
                events.add(MotionEvent.obtain(e));
                return false;
            });
            mActivity.addWindow(mView, p);
        });
        mInstrumentation.waitForIdleSync();

        CtsTouchUtils.emulateTapOnView(mInstrumentation, mActivityRule, mView, size + 5, size + 5);

        assertEquals(1, events.size());
        MotionEvent event = events.iterator().next();
        assertEquals(MotionEvent.ACTION_OUTSIDE, event.getAction());
    }

    @Test
    public void testInjectToStatusBar() {
        // Try to inject event to status bar.
        assumeHasStatusBar(mActivityRule);
        final long downTime = SystemClock.uptimeMillis();
        final MotionEvent eventHover = MotionEvent.obtain(
                downTime, downTime, MotionEvent.ACTION_HOVER_MOVE, 0, 0, 0);
        eventHover.setSource(InputDevice.SOURCE_MOUSE);
        try {
            mInstrumentation.sendPointerSync(eventHover);
            fail("Not allowed to inject event to the window from another process.");
        } catch (SecurityException e) {
            // Should not be allowed to inject event to the window from another process.
        }
    }

    @Test
    public void testInjectFromThread() throws InterruptedException {
        // Continually inject event to activity from thread.
        final Point displaySize = new Point();
        mActivity.getDisplay().getSize(displaySize);
        final Point testPoint = new Point(displaySize.x / 2, displaySize.y / 2);

        final long downTime = SystemClock.uptimeMillis();
        final MotionEvent eventDown = MotionEvent.obtain(
                downTime, downTime, MotionEvent.ACTION_DOWN, testPoint.x, testPoint.y, 1);
        mInstrumentation.sendPointerSync(eventDown);

        final ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.execute(() -> {
            mInstrumentation.sendPointerSync(eventDown);
            for (int i = 0; i < 20; i++) {
                final long eventTime = SystemClock.uptimeMillis();
                final MotionEvent eventMove = MotionEvent.obtain(
                        downTime, eventTime, MotionEvent.ACTION_MOVE, testPoint.x, testPoint.y, 1);
                try {
                    mInstrumentation.sendPointerSync(eventMove);
                } catch (SecurityException e) {
                    fail("Should be allowed to inject event.");
                }
            }
        });

        // Launch another activity, should not crash the process.
        final Intent intent = new Intent(mActivity, TestActivity.class);
        mActivityRule.launchActivity(intent);
        mInstrumentation.waitForIdleSync();

        executor.shutdown();
        executor.awaitTermination(5L, TimeUnit.SECONDS);
    }

    private void waitForWindow(String name) {
        mWmState.waitForWithAmState(state -> state.isWindowSurfaceShown(name),
                name + "'s surface is appeared");
    }

    public static class TestActivity extends Activity {
        private ArrayList<View> mViews = new ArrayList<>();

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
        }

        void addWindow(View view, WindowManager.LayoutParams attrs) {
            getWindowManager().addView(view, attrs);
            mViews.add(view);
        }

        void removeAllWindows() {
            for (View view : mViews) {
                getWindowManager().removeViewImmediate(view);
            }
            mViews.clear();
        }

        @Override
        protected void onPause() {
            super.onPause();
            removeAllWindows();
        }
    }

    /** Set a square window to display at the center of a square layout*/
    static void placeWindowAtLayoutCenter(WindowManager.LayoutParams windowParams, int windowWidth,
            int layoutLeft, int layoutTop, int layoutWidth) {
        windowParams.gravity = Gravity.TOP | Gravity.LEFT;
        int offset = (layoutWidth - windowWidth) / 2;
        windowParams.x = layoutLeft + offset;
        windowParams.y = layoutTop + offset;
    }

    /** Helper class to save, set, and restore pointer location preferences. */
    private static class PointerLocationSession extends SettingsSession<Boolean> {
        PointerLocationSession() {
            super(Settings.System.getUriFor("pointer_location" /* POINTER_LOCATION */),
                    PointerLocationSession::get,
                    PointerLocationSession::put);
        }

        private static void put(ContentResolver contentResolver, String s, boolean v) {
            SystemUtil.runShellCommand(
                    "settings put system " + "pointer_location" + " " + (v ? 1 : 0));
        }

        private static boolean get(ContentResolver contentResolver, String s) {
            try {
                return Integer.parseInt(SystemUtil.runShellCommand(
                        "settings get system " + "pointer_location").trim()) == 1;
            } catch (NumberFormatException e) {
                return false;
            }
        }

        // Wait until pointer location surface shown.
        static void waitForReady(int displayId) {
            final WindowManagerStateHelper wmState = new WindowManagerStateHelper();
            final String windowName = "PointerLocation - display " + displayId;
            wmState.waitForWithAmState(state -> {
                return state.isWindowSurfaceShown(windowName);
            }, windowName + "'s surface is appeared");
        }
    }
}
