chromium/chrome/android/javatests/src/org/chromium/chrome/browser/vr/WebXrVrCardboardInputTest.java

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.vr;

import static org.chromium.chrome.browser.vr.XrTestFramework.PAGE_LOAD_TIMEOUT_S;
import static org.chromium.chrome.browser.vr.XrTestFramework.POLL_TIMEOUT_SHORT_MS;

import android.os.SystemClock;
import android.view.MotionEvent;
import android.view.View;

import androidx.test.filters.MediumTest;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.runner.RunWith;

import org.chromium.base.ThreadUtils;
import org.chromium.base.test.params.ParameterAnnotations.ClassParameter;
import org.chromium.base.test.params.ParameterAnnotations.UseRunnerDelegate;
import org.chromium.base.test.params.ParameterSet;
import org.chromium.base.test.params.ParameterizedRunner;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.vr.rules.XrActivityRestriction;
import org.chromium.chrome.browser.vr.util.VrCardboardTestRuleUtils;
import org.chromium.chrome.test.ChromeActivityTestRule;
import org.chromium.chrome.test.ChromeJUnit4RunnerDelegate;
import org.chromium.components.webxr.CardboardUtils;
import org.chromium.components.webxr.XrSessionCoordinator;

import java.util.List;
import java.util.concurrent.Callable;

/** End-to-end tests for sending input while using WebXR. */
@RunWith(ParameterizedRunner.class)
@UseRunnerDelegate(ChromeJUnit4RunnerDelegate.class)
@CommandLineFlags.Add({
    ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE,
    "enable-features=LogJsConsoleMessages",
    "force-webxr-runtime=cardboard"
})
public class WebXrVrCardboardInputTest {
    @ClassParameter
    private static List<ParameterSet> sClassParams =
            VrCardboardTestRuleUtils.generateDefaultTestRuleParameters();

    @Rule public RuleChain mRuleChain;

    private ChromeActivityTestRule mTestRule;
    private WebXrVrTestFramework mWebXrVrTestFramework;

    public WebXrVrCardboardInputTest(Callable<ChromeActivityTestRule> callable) throws Exception {
        mTestRule = callable.call();
        mRuleChain = VrCardboardTestRuleUtils.wrapRuleInActivityRestrictionRule(mTestRule);
    }

    @Before
    public void setUp() {
        mWebXrVrTestFramework = new WebXrVrTestFramework(mTestRule);
        CardboardUtils.useCardboardV1DeviceParamsForTesting();
    }

    private long sendScreenTouchDownToView(final View view, final int x, final int y) {
        long downTime = SystemClock.uptimeMillis();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    view.dispatchTouchEvent(
                            MotionEvent.obtain(
                                    downTime, downTime, MotionEvent.ACTION_DOWN, x, y, 0));
                });
        return downTime;
    }

    private void sendScreenTouchUpToView(
            final View view, final int x, final int y, final long downTime) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    long now = SystemClock.uptimeMillis();
                    view.dispatchTouchEvent(
                            MotionEvent.obtain(downTime, now, MotionEvent.ACTION_UP, x, y, 0));
                });
    }

    private void sendScreenTapToView(final View view, final int x, final int y) {
        long downTime = sendScreenTouchDownToView(view, x, y);
        SystemClock.sleep(100);
        sendScreenTouchUpToView(view, x, y, downTime);
        SystemClock.sleep(100);
    }

    private void spamScreenTapsToXrSession(
            final XrSessionCoordinator xrSession, final int x, final int y, final int iterations) {
        // Tap the screen a bunch of times.
        // Android doesn't seem to like sending touch events too quickly, so have a short delay
        // between events.
        for (int i = 0; i < iterations; i++) {
            sendScreenTapToXrSession(xrSession, x, y);
        }
    }

    private void sendScreenTapToXrSession(
            final XrSessionCoordinator xrSession, final int x, final int y) {
        sendScreenTouchDownToXrSession(xrSession, x, y);
        SystemClock.sleep(100);
        sendScreenTouchUpToXrSession(xrSession, x, y);
        SystemClock.sleep(100);
    }

    private void sendScreenTouchDownToXrSession(
            final XrSessionCoordinator xrSession, final int x, final int y) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    xrSession.onDrawingSurfaceTouch(true, true, 0, x, y);
                });
    }

    private void sendScreenTouchUpToXrSession(
            final XrSessionCoordinator xrSession, final int x, final int y) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    xrSession.onDrawingSurfaceTouch(true, false, 0, x, y);
                });
    }

    /**
     * Tests that screen touches are registered as XR input in immersive sessions, when the viewer
     * is Cardboard.
     */
    @Test
    @MediumTest
    @CommandLineFlags.Add({"enable-features=WebXR,Cardboard"})
    @XrActivityRestriction({XrActivityRestriction.SupportedActivity.ALL})
    public void testScreenTapsRegistered_WebXr() {
        mWebXrVrTestFramework.loadFileAndAwaitInitialization(
                "test_webxr_input", PAGE_LOAD_TIMEOUT_S);
        mWebXrVrTestFramework.enterSessionWithUserGestureOrFail();

        int numIterations = 5;
        mWebXrVrTestFramework.runJavaScriptOrFail(
                "stepSetupListeners(" + String.valueOf(numIterations) + ")", POLL_TIMEOUT_SHORT_MS);

        XrSessionCoordinator activeSession = XrSessionCoordinator.getActiveInstanceForTesting();
        int x = mWebXrVrTestFramework.getCurrentContentView().getWidth() / 2;
        int y = mWebXrVrTestFramework.getCurrentContentView().getHeight() / 2;

        for (int i = 0; i < numIterations; i++) {
            sendScreenTapToXrSession(activeSession, x, y);
            mWebXrVrTestFramework.waitOnJavaScriptStep();
        }

        mWebXrVrTestFramework.endTest();
    }

    /**
     * Tests that screen touches are registered as transient XR input in inline sessions, when the
     * viewer is Cardboard.
     */
    @Test
    @MediumTest
    @CommandLineFlags.Add({"enable-features=WebXR,Cardboard"})
    @XrActivityRestriction({XrActivityRestriction.SupportedActivity.ALL})
    public void testTransientScreenTapsRegistered_WebXr() {
        mWebXrVrTestFramework.loadFileAndAwaitInitialization(
                "test_webxr_transient_input", PAGE_LOAD_TIMEOUT_S);

        int numIterations = 10;
        View presentationView = mWebXrVrTestFramework.getCurrentContentView();
        mWebXrVrTestFramework.runJavaScriptOrFail(
                "stepSetupListeners(" + String.valueOf(numIterations) + ")", POLL_TIMEOUT_SHORT_MS);

        int x = presentationView.getWidth() / 2;
        int y = presentationView.getHeight() / 2;

        // Tap the screen a bunch of times and make sure that they're all registered. Ideally, we
        // shouldn't have to ack each one, but it's possible for inputs to get eaten by garbage
        // collection if there are multiple in flight, so only send one at a time.
        for (int i = 0; i < numIterations; i++) {
            sendScreenTapToView(presentationView, x, y);
            mWebXrVrTestFramework.waitOnJavaScriptStep();
        }

        mWebXrVrTestFramework.endTest();
    }

    /**
     * Tests that focus is locked to the device with an immersive session for the purposes of VR
     * input.
     */
    @Test
    @MediumTest
    @CommandLineFlags.Add({"enable-features=WebXR,Cardboard"})
    @XrActivityRestriction({XrActivityRestriction.SupportedActivity.ALL})
    public void testPresentationLocksFocus_WebXr() {
        mWebXrVrTestFramework.loadFileAndAwaitInitialization(
                "webxr_test_presentation_locks_focus", PAGE_LOAD_TIMEOUT_S);
        mWebXrVrTestFramework.enterSessionWithUserGestureOrFail();
        mWebXrVrTestFramework.executeStepAndWait("stepSetupFocusLoss()");
        mWebXrVrTestFramework.endTest();
    }

    /**
     * Verifies that the XRSession has an input source when using WebXR and Cardboard. There should
     * be no gamepads on the input source or navigator array.
     */
    @Test
    @MediumTest
    @CommandLineFlags.Add({"enable-features=WebXR,Cardboard"})
    @XrActivityRestriction({XrActivityRestriction.SupportedActivity.ALL})
    public void testWebXrInputSourceWithoutGamepad() {
        mWebXrVrTestFramework.loadFileAndAwaitInitialization(
                "test_webxr_gamepad_support", PAGE_LOAD_TIMEOUT_S);
        mWebXrVrTestFramework.enterSessionWithUserGestureOrFail();

        // Spam input to make sure the Gamepad API registers the gamepad if it should.
        int numIterations = 10;
        int x = mWebXrVrTestFramework.getCurrentContentView().getWidth() / 2;
        int y = mWebXrVrTestFramework.getCurrentContentView().getHeight() / 2;

        spamScreenTapsToXrSession(
                XrSessionCoordinator.getActiveInstanceForTesting(), x, y, numIterations);

        mWebXrVrTestFramework.pollJavaScriptBooleanOrFail(
                "inputSourceHasNoGamepad()", POLL_TIMEOUT_SHORT_MS);

        mWebXrVrTestFramework.runJavaScriptOrFail("done()", POLL_TIMEOUT_SHORT_MS);
        mWebXrVrTestFramework.endTest();
    }
}