Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 0e81c75a authored by Antonio Kantek's avatar Antonio Kantek Committed by Android (Google) Code Review
Browse files

Merge "Move concurrent multi user IME tests from fw/base to cts" into main

parents 3a84e2fa fd72cc70
Loading
Loading
Loading
Loading
+0 −53
Original line number Diff line number Diff line
// 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 {
    default_team: "trendy_team_input_method_framework",
    default_applicable_licenses: ["frameworks_base_license"],
}

android_test {
    name: "ConcurrentMultiSessionImeTest",
    srcs: ["src/**/*.java"],
    resource_dirs: ["res"],
    libs: ["android.test.runner.stubs"],
    static_libs: [
        "androidx.core_core",
        "androidx.test.ext.junit",
        "androidx.test.rules",
        "compatibility-device-util-axt",
        "platform-test-annotations",
        "platform-test-rules",
        "truth",

        // beadstead
        "Nene",
        "Harrier",
        "TestApp",
    ],
    test_suites: [
        "general-tests",
        // This is an equivalent of general-tests for automotive.
        // It helps manage the build time on automotive branches.
        "automotive-general-tests",
    ],
    sdk_version: "test_current",

    data: [
        ":CtsMockInputMethod",
    ],

    // Store test artifacts in separated directories for easier debugging.
    per_testcase_directory: true,
}
+0 −34
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?><!--
  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.
-->

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.android.server.inputmethod.multisessiontest">

    <application>
        <uses-library android:name="android.test.runner" />
        <activity android:name=".ConcurrentMultiUserTestActivity"
                  android:theme="@android:style/Theme.Material.NoActionBar"
                  android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
            </intent-filter>
        </activity>
    </application>

    <instrumentation
        android:name="androidx.test.runner.AndroidJUnitRunner"
        android:targetPackage="com.android.server.inputmethod.multisessiontest"></instrumentation>
</manifest>
+0 −52
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?><!--
  ~ 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.
  -->

<configuration description="Config for Concurrent Multi-Session IME tests">
    <object class="com.android.tradefed.testtype.suite.module.DeviceFeatureModuleController"
        type="module_controller">
        <!-- TODO(b/323372972): require this feature once the bug is fixed. -->
        <!-- option name="required-feature" value="android.software.input_methods" -->

        <!-- Currently enabled to automotive only -->
        <option name="required-feature" value="android.hardware.type.automotive" />
    </object>
    <option name="test-suite-tag" value="apct" />

    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
        <option name="cleanup-apks" value="true" />
        <option name="force-install-mode" value="FULL" />
        <option name="test-file-name" value="ConcurrentMultiSessionImeTest.apk" />
        <option name="test-file-name" value="CtsMockInputMethod.apk" />
    </target_preparer>

    <!-- RunOnSecondaryUserTargetPreparer must run after SuiteApkInstaller. -->
    <target_preparer class="com.android.tradefed.targetprep.RunOnSecondaryUserTargetPreparer">
        <option name="start-background-user" value="true" />
        <option name="test-package-name" value="com.android.server.inputmethod.multisessiontest" />
        <option name="test-package-name" value="com.android.cts.mockime" />
    </target_preparer>

    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
        <option name="run-command" value="setprop debug.wm.disable_deprecated_abi_dialog 1" />
        <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1" />
        <option name="teardown-command"
            value="settings delete secure show_ime_with_hard_keyboard" />
    </target_preparer>

    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
        <option name="package" value="com.android.server.inputmethod.multisessiontest" />
    </test>
</configuration>
+0 −27
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
    Copyright 2024 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.
-->
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">
    <EditText
        android:id="@+id/edit_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:hint="Input text here"/>
</FrameLayout>
+0 −367
Original line number Diff line number Diff line
/*
 * 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 com.android.server.inputmethod.multisessiontest;

import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;

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

import static com.android.compatibility.common.util.concurrentuser.ConcurrentUserActivityUtils.getResponderUserId;
import static com.android.compatibility.common.util.concurrentuser.ConcurrentUserActivityUtils.launchActivityAsUserSync;
import static com.android.compatibility.common.util.concurrentuser.ConcurrentUserActivityUtils.sendBundleAndWaitForReply;
import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.KEY_DISPLAY_ID;
import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.KEY_EDITTEXT_CENTER;
import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.KEY_IME_SHOWN;
import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.KEY_REQUEST_CODE;
import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REQUEST_DISPLAY_ID;
import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REQUEST_EDITTEXT_POSITION;
import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REQUEST_HIDE_IME;
import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REQUEST_IME_STATUS;
import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REQUEST_SHOW_IME;

import static com.google.common.truth.Truth.assertWithMessage;

import static org.junit.Assume.assumeTrue;

import android.app.UiAutomation;
import android.content.ComponentName;
import android.content.Context;
import android.os.Bundle;
import android.os.UserHandle;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodManager;

import androidx.test.core.app.ActivityScenario;

import com.android.bedstead.harrier.BedsteadJUnit4;
import com.android.bedstead.harrier.DeviceState;
import com.android.bedstead.harrier.annotations.RequireAutomotive;
import com.android.bedstead.multiuser.annotations.RequireVisibleBackgroundUsers;
import com.android.compatibility.common.util.SystemUtil;

import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.io.IOException;
import java.util.List;

@RunWith(BedsteadJUnit4.class)
@RequireVisibleBackgroundUsers(reason = "This test requires a background visible user in addition"
        + " to the current visible user to test concurrent multi-user IME scenarios")
@RequireAutomotive(reason = "Visible background users are currently only supported on automotive")
public final class ConcurrentMultiUserTest {

    @ClassRule
    @Rule
    public static final DeviceState sDeviceState = new DeviceState();

    private static final ComponentName TEST_ACTIVITY = new ComponentName(
            getInstrumentation().getTargetContext().getPackageName(),
            ConcurrentMultiUserTestActivity.class.getName());
    private final Context mContext = getInstrumentation().getTargetContext();
    private final InputMethodManager mInputMethodManager =
            mContext.getSystemService(InputMethodManager.class);
    private final UiAutomation mUiAutomation = getInstrumentation().getUiAutomation();

    private ActivityScenario<ConcurrentMultiUserTestActivity> mActivityScenario;
    private ConcurrentMultiUserTestActivity mActivity;
    private int mPeerUserId;

    @Before
    public void setUp() {
        // Launch passenger activity.
        mPeerUserId = getResponderUserId();
        launchActivityAsUserSync(TEST_ACTIVITY, mPeerUserId);

        // Launch driver activity.
        mActivityScenario = ActivityScenario.launch(ConcurrentMultiUserTestActivity.class);
        mActivityScenario.onActivity(activity -> mActivity = activity);
        mUiAutomation.adoptShellPermissionIdentity(INTERACT_ACROSS_USERS_FULL);
    }

    @After
    public void tearDown() {
        mUiAutomation.dropShellPermissionIdentity();
        if (mActivityScenario != null) {
            mActivityScenario.close();
        }
    }

    @Test
    public void driverShowImeNotAffectPassenger() throws Exception {
        assertDriverImeHidden();
        assertPassengerImeHidden();

        showDriverImeAndAssert();
        assertPassengerImeHidden();
    }

    @Test
    @Ignore("b/352823913")
    public void passengerShowImeNotAffectDriver() throws Exception {
        assertDriverImeHidden();
        assertPassengerImeHidden();

        showPassengerImeAndAssert();
        assertDriverImeHidden();
    }

    @Test
    public void driverHideImeNotAffectPassenger() throws Exception {
        showDriverImeAndAssert();
        showPassengerImeAndAssert();

        hideDriverImeAndAssert();
        assertPassengerImeShown();
    }

    @Test
    public void passengerHideImeNotAffectDriver() throws Exception {
        showDriverImeAndAssert();
        showPassengerImeAndAssert();

        hidePassengerImeAndAssert();
        assertDriverImeShown();
    }

    @Test
    public void imeListNotEmpty() {
        List<InputMethodInfo> driverImeList = mInputMethodManager.getInputMethodList();
        assertWithMessage("Driver IME list shouldn't be empty")
                .that(driverImeList.isEmpty()).isFalse();

        List<InputMethodInfo> passengerImeList =
                mInputMethodManager.getInputMethodListAsUser(mPeerUserId);
        assertWithMessage("Passenger IME list shouldn't be empty")
                .that(passengerImeList.isEmpty()).isFalse();
    }

    @Test
    public void enabledImeListNotEmpty() {
        List<InputMethodInfo> driverEnabledImeList =
                mInputMethodManager.getEnabledInputMethodList();
        assertWithMessage("Driver enabled IME list shouldn't be empty")
                .that(driverEnabledImeList.isEmpty()).isFalse();

        List<InputMethodInfo> passengerEnabledImeList =
                mInputMethodManager.getEnabledInputMethodListAsUser(UserHandle.of(mPeerUserId));
        assertWithMessage("Passenger enabled IME list shouldn't be empty")
                .that(passengerEnabledImeList.isEmpty()).isFalse();
    }

    @Test
    public void currentImeNotNull() {
        InputMethodInfo driverIme = mInputMethodManager.getCurrentInputMethodInfo();
        assertWithMessage("Driver IME shouldn't be null").that(driverIme).isNotNull();

        InputMethodInfo passengerIme =
                mInputMethodManager.getCurrentInputMethodInfoAsUser(UserHandle.of(mPeerUserId));
        assertWithMessage("Passenger IME shouldn't be null")
                .that(passengerIme).isNotNull();
    }

    @Test
    public void enableDisableImePerUser() throws IOException {
        UserHandle driver = UserHandle.of(mContext.getUserId());
        UserHandle passenger = UserHandle.of(mPeerUserId);
        enableDisableImeForUser(driver, passenger);
        enableDisableImeForUser(passenger, driver);
    }

    @Test
    public void setImePerUser() throws IOException {
        UserHandle driver = UserHandle.of(mContext.getUserId());
        UserHandle passenger = UserHandle.of(mPeerUserId);
        setImeForUser(driver, passenger);
        setImeForUser(passenger, driver);
    }

    private void assertDriverImeShown() {
        assertWithMessage("Driver IME should be shown")
                .that(mActivity.isMyImeVisible()).isTrue();
    }

    private void assertDriverImeHidden() {
        assertWithMessage("Driver IME should be hidden")
                .that(mActivity.isMyImeVisible()).isFalse();
    }

    private void assertPassengerImeHidden() {
        final Bundle bundleToSend = new Bundle();
        bundleToSend.putInt(KEY_REQUEST_CODE, REQUEST_IME_STATUS);
        Bundle receivedBundle = sendBundleAndWaitForReply(TEST_ACTIVITY.getPackageName(),
                mPeerUserId, bundleToSend);
        assertWithMessage("Passenger IME should be hidden")
                .that(receivedBundle.getBoolean(KEY_IME_SHOWN, /* defaultValue= */ true)).isFalse();
    }

    private void assertPassengerImeShown() {
        final Bundle bundleToSend = new Bundle();
        bundleToSend.putInt(KEY_REQUEST_CODE, REQUEST_IME_STATUS);
        Bundle receivedBundle = sendBundleAndWaitForReply(TEST_ACTIVITY.getPackageName(),
                mPeerUserId, bundleToSend);
        assertWithMessage("Passenger IME should be shown")
                .that(receivedBundle.getBoolean(KEY_IME_SHOWN)).isTrue();
    }

    private void showDriverImeAndAssert() throws Exception {
        //  WindowManagerInternal only allows the top focused display to show IME, so this method
        //  taps the driver display in case it is not the top focused display.
        moveDriverDisplayToTop();

        mActivity.showMyImeAndWait();
    }

    private void hideDriverImeAndAssert() {
        mActivity.hideMyImeAndWait();
    }

    private void showPassengerImeAndAssert() throws Exception {
        // WindowManagerInternal only allows the top focused display to show IME, so this method
        // taps the passenger display in case it is not the top focused display.
        movePassengerDisplayToTop();

        Bundle bundleToSend = new Bundle();
        bundleToSend.putInt(KEY_REQUEST_CODE, REQUEST_SHOW_IME);
        Bundle receivedBundle = sendBundleAndWaitForReply(TEST_ACTIVITY.getPackageName(),
                mPeerUserId, bundleToSend);

        assertWithMessage("Passenger IME should be shown")
                .that(receivedBundle.getBoolean(KEY_IME_SHOWN)).isTrue();
    }

    private void hidePassengerImeAndAssert() {
        Bundle bundleToSend = new Bundle();
        bundleToSend.putInt(KEY_REQUEST_CODE, REQUEST_HIDE_IME);
        Bundle receivedBundle = sendBundleAndWaitForReply(TEST_ACTIVITY.getPackageName(),
                mPeerUserId, bundleToSend);

        assertWithMessage("Passenger IME should be hidden")
                .that(receivedBundle.getBoolean(KEY_IME_SHOWN, /* defaultValue= */ true)).isFalse();
    }

    private void moveDriverDisplayToTop() throws Exception {
        float[] driverEditTextCenter = mActivity.getEditTextCenter();
        SystemUtil.runShellCommand(mUiAutomation, String.format("input tap %f %f",
                driverEditTextCenter[0], driverEditTextCenter[1]));
    }

    private void movePassengerDisplayToTop() throws Exception {
        final Bundle bundleToSend = new Bundle();
        bundleToSend.putInt(KEY_REQUEST_CODE, REQUEST_EDITTEXT_POSITION);
        Bundle receivedBundle = sendBundleAndWaitForReply(TEST_ACTIVITY.getPackageName(),
                mPeerUserId, bundleToSend);
        final float[] passengerEditTextCenter = receivedBundle.getFloatArray(KEY_EDITTEXT_CENTER);

        bundleToSend.putInt(KEY_REQUEST_CODE, REQUEST_DISPLAY_ID);
        receivedBundle = sendBundleAndWaitForReply(TEST_ACTIVITY.getPackageName(),
                mPeerUserId, bundleToSend);
        final int passengerDisplayId = receivedBundle.getInt(KEY_DISPLAY_ID);
        SystemUtil.runShellCommand(mUiAutomation, String.format("input -d %d tap %f %f",
                passengerDisplayId, passengerEditTextCenter[0], passengerEditTextCenter[1]));
    }

    /**
     * Disables/enables IME for {@code user1}, then verifies that the IME settings for {@code user1}
     * has changed as expected and {@code user2} stays the same.
     */
    private void enableDisableImeForUser(UserHandle user1, UserHandle user2) throws IOException {
        List<InputMethodInfo> user1EnabledImeList =
                mInputMethodManager.getEnabledInputMethodListAsUser(user1);
        List<InputMethodInfo> user2EnabledImeList =
                mInputMethodManager.getEnabledInputMethodListAsUser(user2);

        // Disable an IME for user1.
        InputMethodInfo imeToDisable = user1EnabledImeList.get(0);
        SystemUtil.runShellCommand(mUiAutomation,
                "ime disable --user " + user1.getIdentifier() + " " + imeToDisable.getId());
        List<InputMethodInfo> user1EnabledImeList2 =
                mInputMethodManager.getEnabledInputMethodListAsUser(user1);
        List<InputMethodInfo> user2EnabledImeList2 =
                mInputMethodManager.getEnabledInputMethodListAsUser(user2);
        assertWithMessage("User " + user1.getIdentifier() + " IME " + imeToDisable.getId()
                + " should be disabled")
                .that(user1EnabledImeList2.contains(imeToDisable)).isFalse();
        assertWithMessage("Disabling user " + user1.getIdentifier()
                + " IME shouldn't affect user " + user2.getIdentifier())
                .that(user2EnabledImeList2.containsAll(user2EnabledImeList)
                        && user2EnabledImeList.containsAll(user2EnabledImeList2))
                .isTrue();

        // Enable the IME.
        SystemUtil.runShellCommand(mUiAutomation,
                "ime enable --user " + user1.getIdentifier() + " " + imeToDisable.getId());
        List<InputMethodInfo> user1EnabledImeList3 =
                mInputMethodManager.getEnabledInputMethodListAsUser(user1);
        List<InputMethodInfo> user2EnabledImeList3 =
                mInputMethodManager.getEnabledInputMethodListAsUser(user2);
        assertWithMessage("User " + user1.getIdentifier() + " IME " + imeToDisable.getId()
                + " should be enabled").that(user1EnabledImeList3.contains(imeToDisable)).isTrue();
        assertWithMessage("Enabling user " + user1.getIdentifier()
                + " IME shouldn't affect user " + user2.getIdentifier())
                .that(user2EnabledImeList2.containsAll(user2EnabledImeList3)
                        && user2EnabledImeList3.containsAll(user2EnabledImeList2))
                .isTrue();
    }

    /**
     * Sets/resets IME for {@code user1}, then verifies that the IME settings for {@code user1}
     * has changed as expected and {@code user2} stays the same.
     */
    private void setImeForUser(UserHandle user1, UserHandle user2) throws IOException {
        // Reset IME for user1.
        SystemUtil.runShellCommand(mUiAutomation,
                "ime reset --user " + user1.getIdentifier());

        List<InputMethodInfo> user1EnabledImeList =
                mInputMethodManager.getEnabledInputMethodListAsUser(user1);
        assumeTrue("There must be at least two IME to test", user1EnabledImeList.size() >= 2);
        InputMethodInfo user1Ime = mInputMethodManager.getCurrentInputMethodInfoAsUser(user1);
        InputMethodInfo user2Ime = mInputMethodManager.getCurrentInputMethodInfoAsUser(user2);

        // Set to another IME for user1.
        InputMethodInfo anotherIme = null;
        for (InputMethodInfo info : user1EnabledImeList) {
            if (!info.equals(user1Ime)) {
                anotherIme = info;
            }
        }
        SystemUtil.runShellCommand(mUiAutomation,
                "ime set --user " + user1.getIdentifier() + " " + anotherIme.getId());
        InputMethodInfo user1Ime2 = mInputMethodManager.getCurrentInputMethodInfoAsUser(user1);
        InputMethodInfo user2Ime2 = mInputMethodManager.getCurrentInputMethodInfoAsUser(user2);
        assertWithMessage("The current IME for user " + user1.getIdentifier() + " is wrong")
                .that(user1Ime2).isEqualTo(anotherIme);
        assertWithMessage("The current IME for user " + user2.getIdentifier() + " shouldn't change")
                .that(user2Ime2).isEqualTo(user2Ime);

        // Reset IME for user1.
        SystemUtil.runShellCommand(mUiAutomation,
                "ime reset --user " + user1.getIdentifier());
        InputMethodInfo user1Ime3 = mInputMethodManager.getCurrentInputMethodInfoAsUser(user1);
        InputMethodInfo user2Ime3 = mInputMethodManager.getCurrentInputMethodInfoAsUser(user2);
        assertWithMessage("The current IME for user " + user1.getIdentifier() + " is wrong")
                .that(user1Ime3).isEqualTo(user1Ime);
        assertWithMessage("The current IME for user " + user2.getIdentifier() + " shouldn't change")
                .that(user2Ime3).isEqualTo(user2Ime);
    }
}
Loading