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

Commit 8e023a00 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add test app SimpleTestIme to test InputMethodService"

parents bef3da31 640b2b79
Loading
Loading
Loading
Loading
+50 −1
Original line number Diff line number Diff line
@@ -28,7 +28,7 @@ android_test {
    ],

    srcs: [
        "src/**/*.java",
        "src/server/**/*.java",
    ],

    static_libs: [
@@ -60,3 +60,52 @@ android_test {
        enabled: false,
    },
}

android_test {
    name: "FrameworksImeTests",
    defaults: [
        "modules-utils-testable-device-config-defaults",
    ],

    srcs: [
        "src/com/android/inputmethodservice/**/*.java",
    ],

    manifest: "src/com/android/inputmethodservice/AndroidManifest.xml",
    test_config: "src/com/android/inputmethodservice/AndroidTest.xml",

    static_libs: [
        "androidx.test.core",
        "androidx.test.runner",
        "androidx.test.espresso.core",
        "androidx.test.espresso.contrib",
        "androidx.test.ext.truth",
        "frameworks-base-testutils",
        "mockito-target-extended-minus-junit4",
        "platform-test-annotations",
        "services.core",
        "servicestests-core-utils",
        "servicestests-utils-mockito-extended",
        "truth-prebuilt",
        "SimpleImeTestingLib",
        "SimpleImeImsLib",
    ],

    libs: [
        "android.test.mock",
        "android.test.base",
        "android.test.runner",
    ],

    data: [
        ":SimpleTestIme",
    ],

    certificate: "platform",
    platform_apis: true,
    test_suites: ["device-tests"],

    optimize: {
        enabled: false,
    },
}
+43 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright (C) 2022 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.inputmethod.imetests">

    <uses-sdk android:targetSdkVersion="31" />

    <!-- Permissions required for granting and logging -->
    <uses-permission android:name="android.permission.LOG_COMPAT_CHANGE"/>
    <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG"/>
    <uses-permission android:name="android.permission.OVERRIDE_COMPAT_CHANGE_CONFIG"/>
    <uses-permission android:name="android.permission.OVERRIDE_COMPAT_CHANGE_CONFIG_ON_RELEASE_BUILD"/>

    <!-- Permissions for reading system info -->
    <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />

    <application android:debuggable="true">
        <uses-library android:name="android.test.runner" />
    </application>

    <!-- The "targetPackage" reference the instruments APK package, which is the FakeImeApk, while
    the test package is "com.android.inputmethod.imetests" (FrameworksImeTests.apk).-->
    <instrumentation
        android:name="androidx.test.runner.AndroidJUnitRunner"
        android:targetPackage="com.android.apps.inputmethod.simpleime"
        android:label="Frameworks IME Tests" />
</manifest>
+41 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright (C) 2022 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="Runs Frameworks IME Tests.">
    <option name="test-suite-tag" value="apct" />
    <option name="test-suite-tag" value="apct-instrumentation" />

    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
        <option name="cleanup-apks" value="true" />
        <option name="install-arg" value="-t" />
        <option name="test-file-name" value="FrameworksImeTests.apk" />
        <option name="test-file-name" value="SimpleTestIme.apk" />
    </target_preparer>

    <option name="test-tag" value="FrameworksImeTests" />

    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
        <option name="package" value="com.android.inputmethod.imetests" />
        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
        <option name="hidden-api-checks" value="false"/>
    </test>

    <!-- Collect the files in the dump directory for debugging -->
    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
        <option name="directory-keys" value="/sdcard/FrameworksImeTests/" />
        <option name="collect-on-run-ended-only" value="true" />
    </metrics_collector>
</configuration>
+276 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.inputmethodservice;

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

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

import android.app.Instrumentation;
import android.content.Context;
import android.content.res.Configuration;
import android.os.RemoteException;
import android.support.test.uiautomator.By;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject2;
import android.support.test.uiautomator.Until;
import android.util.Log;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import androidx.test.platform.app.InstrumentationRegistry;

import com.android.apps.inputmethod.simpleime.ims.InputMethodServiceWrapper;
import com.android.apps.inputmethod.simpleime.testing.TestActivity;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

@RunWith(AndroidJUnit4.class)
@MediumTest
public class InputMethodServiceTest {
    private static final String TAG = "SimpleIMSTest";
    private static final String INPUT_METHOD_SERVICE_NAME = ".SimpleInputMethodService";
    private static final String EDIT_TEXT_DESC = "Input box";
    private static final long TIMEOUT_IN_SECONDS = 3;

    public Instrumentation mInstrumentation;
    public UiDevice mUiDevice;
    public Context mContext;
    public String mTargetPackageName;
    public TestActivity mActivity;
    public EditText mEditText;
    public InputMethodServiceWrapper mInputMethodService;
    public String mInputMethodId;

    @Before
    public void setUp() throws Exception {
        mInstrumentation = InstrumentationRegistry.getInstrumentation();
        mUiDevice = UiDevice.getInstance(mInstrumentation);
        mContext = mInstrumentation.getContext();
        mTargetPackageName = mInstrumentation.getTargetContext().getPackageName();
        mInputMethodId = getInputMethodId();
        prepareIme();
        prepareEditor();

        // Waits for input binding ready.
        eventually(
                () -> {
                    mInputMethodService =
                            InputMethodServiceWrapper.getInputMethodServiceWrapperForTesting();
                    assertThat(mInputMethodService).isNotNull();

                    // The editor won't bring up keyboard by default.
                    assertThat(mInputMethodService.getCurrentInputStarted()).isTrue();
                    assertThat(mInputMethodService.getCurrentInputViewStarted()).isFalse();
                });
    }

    @Test
    public void testShowHideKeyboard_byUserAction() throws InterruptedException {
        // Performs click on editor box to bring up the soft keyboard.
        Log.i(TAG, "Click on EditText.");
        verifyInputViewStatus(() -> clickOnEditorText(), true /* inputViewStarted */);

        // Press back key to hide soft keyboard.
        Log.i(TAG, "Press back");
        verifyInputViewStatus(
                () -> assertThat(mUiDevice.pressHome()).isTrue(), false /* inputViewStarted */);
    }

    @Test
    public void testShowHideKeyboard_byApi() throws InterruptedException {
        // Triggers to show IME via public API.
        verifyInputViewStatus(
                () -> assertThat(mActivity.showImeWithWindowInsetsController()).isTrue(),
                true /* inputViewStarted */);

        // Triggers to hide IME via public API.
        // TODO(b/242838873): investigate why WIC#hide(ime()) does not work, likely related to
        //  triggered from IME process.
        verifyInputViewStatusOnMainSync(
                () -> assertThat(mActivity.hideImeWithInputMethodManager(0 /* flags */)).isTrue(),
                false /* inputViewStarted */);
    }

    @Test
    public void testShowHideSelf() throws InterruptedException {
        // IME requests to show itself without any flags: expect shown.
        Log.i(TAG, "Call IMS#requestShowSelf(0)");
        verifyInputViewStatusOnMainSync(
                () -> mInputMethodService.requestShowSelf(0), true /* inputViewStarted */);

        // IME requests to hide itself with flag: HIDE_IMPLICIT_ONLY, expect not hide (shown).
        Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)");
        verifyInputViewStatusOnMainSync(
                () -> mInputMethodService.requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY),
                true /* inputViewStarted */);

        // IME request to hide itself without any flags: expect hidden.
        Log.i(TAG, "Call IMS#requestHideSelf(0)");
        verifyInputViewStatusOnMainSync(
                () -> mInputMethodService.requestHideSelf(0), false /* inputViewStarted */);

        // IME request to show itself with flag SHOW_IMPLICIT: expect shown.
        Log.i(TAG, "Call IMS#requestShowSelf(InputMethodManager.SHOW_IMPLICIT)");
        verifyInputViewStatusOnMainSync(
                () -> mInputMethodService.requestShowSelf(InputMethodManager.SHOW_IMPLICIT),
                true /* inputViewStarted */);

        // IME request to hide itself with flag: HIDE_IMPLICIT_ONLY, expect hidden.
        Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)");
        verifyInputViewStatusOnMainSync(
                () -> mInputMethodService.requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY),
                false /* inputViewStarted */);
    }

    private void verifyInputViewStatus(Runnable runnable, boolean inputViewStarted)
            throws InterruptedException {
        verifyInputViewStatusInternal(runnable, inputViewStarted, false /*runOnMainSync*/);
    }

    private void verifyInputViewStatusOnMainSync(Runnable runnable, boolean inputViewStarted)
            throws InterruptedException {
        verifyInputViewStatusInternal(runnable, inputViewStarted, true /*runOnMainSync*/);
    }

    private void verifyInputViewStatusInternal(
            Runnable runnable, boolean inputViewStarted, boolean runOnMainSync)
            throws InterruptedException {
        CountDownLatch signal = new CountDownLatch(1);
        mInputMethodService.setCountDownLatchForTesting(signal);
        // Runnable to trigger onStartInputView()/ onFinishInputView()
        if (runOnMainSync) {
            mInstrumentation.runOnMainSync(runnable);
        } else {
            runnable.run();
        }
        // Waits for onStartInputView() to finish.
        mInstrumentation.waitForIdleSync();
        signal.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
        // Input is not finished.
        assertThat(mInputMethodService.getCurrentInputStarted()).isTrue();
        assertThat(mInputMethodService.getCurrentInputViewStarted()).isEqualTo(inputViewStarted);
    }

    @Test
    public void testFullScreenMode() throws Exception {
        Log.i(TAG, "Set orientation natural");
        verifyFullscreenMode(() -> setOrientation(0), true /* orientationPortrait */);

        Log.i(TAG, "Set orientation left");
        verifyFullscreenMode(() -> setOrientation(1), false /* orientationPortrait */);

        Log.i(TAG, "Set orientation right");
        verifyFullscreenMode(() -> setOrientation(2), false /* orientationPortrait */);

        mUiDevice.unfreezeRotation();
    }

    private void setOrientation(int orientation) {
        // Simple wrapper for catching RemoteException.
        try {
            switch (orientation) {
                case 1:
                    mUiDevice.setOrientationLeft();
                    break;
                case 2:
                    mUiDevice.setOrientationRight();
                    break;
                default:
                    mUiDevice.setOrientationNatural();
            }
        } catch (RemoteException e) {
            throw new RuntimeException(e);
        }
    }

    private void verifyFullscreenMode(Runnable runnable, boolean orientationPortrait)
            throws InterruptedException {
        CountDownLatch signal = new CountDownLatch(1);
        mInputMethodService.setCountDownLatchForTesting(signal);

        // Runnable to trigger onConfigurationChanged()
        try {
            runnable.run();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        // Waits for onConfigurationChanged() to finish.
        mInstrumentation.waitForIdleSync();
        signal.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);

        clickOnEditorText();
        eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isTrue());

        assertThat(mInputMethodService.getResources().getConfiguration().orientation)
                .isEqualTo(
                        orientationPortrait
                                ? Configuration.ORIENTATION_PORTRAIT
                                : Configuration.ORIENTATION_LANDSCAPE);
        EditorInfo editorInfo = mInputMethodService.getCurrentInputEditorInfo();
        assertThat(editorInfo.imeOptions & EditorInfo.IME_FLAG_NO_FULLSCREEN).isEqualTo(0);
        assertThat(editorInfo.internalImeOptions & EditorInfo.IME_INTERNAL_FLAG_APP_WINDOW_PORTRAIT)
                .isEqualTo(
                        orientationPortrait ? EditorInfo.IME_INTERNAL_FLAG_APP_WINDOW_PORTRAIT : 0);
        assertThat(mInputMethodService.onEvaluateFullscreenMode()).isEqualTo(!orientationPortrait);
        assertThat(mInputMethodService.isFullscreenMode()).isEqualTo(!orientationPortrait);

        mUiDevice.pressBack();
    }

    private void prepareIme() throws Exception {
        executeShellCommand("ime enable " + mInputMethodId);
        executeShellCommand("ime set " + mInputMethodId);
        mInstrumentation.waitForIdleSync();
        Log.i(TAG, "Finish preparing IME");
    }

    private void prepareEditor() {
        mActivity = TestActivity.start(mInstrumentation);
        mEditText = mActivity.mEditText;
        Log.i(TAG, "Finish preparing activity with editor.");
    }

    private String getInputMethodId() {
        return mTargetPackageName + "/" + INPUT_METHOD_SERVICE_NAME;
    }

    private String executeShellCommand(String cmd) throws Exception {
        Log.i(TAG, "Run command: " + cmd);
        return UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
                .executeShellCommand(cmd);
    }

    private void clickOnEditorText() {
        // Find the editText and click it.
        UiObject2 editTextUiObject =
                mUiDevice.wait(
                        Until.findObject(By.desc(EDIT_TEXT_DESC)),
                        TimeUnit.SECONDS.toMillis(TIMEOUT_IN_SECONDS));
        assertThat(editTextUiObject).isNotNull();
        editTextUiObject.click();
        mInstrumentation.waitForIdleSync();
    }
}
+62 −0
Original line number Diff line number Diff line
// Copyright (C) 2022 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 {
    // See: http://go/android-license-faq
    // A large-scale-change added 'default_applicable_licenses' to import
    // all of the 'license_kinds' from "frameworks_base_license"
    // to get the below license kinds:
    //   SPDX-license-identifier-Apache-2.0
    default_applicable_licenses: ["frameworks_base_license"],
}

android_test_helper_app {
    name: "SimpleTestIme",

    srcs: [
        "src/com/android/apps/inputmethod/simpleime/*.java",
    ],

    static_libs: [
        "SimpleImeImsLib",
        "SimpleImeTestingLib",
    ],
    resource_dirs: ["res"],
    manifest: "AndroidManifest.xml",

    dex_preopt: {
        enabled: false,
    },
    optimize: {
        enabled: false,
    },
    export_package_resources: true,
    sdk_version: "current",
}

android_library {
    name: "SimpleImeImsLib",
    srcs: [
        "src/com/android/apps/inputmethod/simpleime/ims/*.java",
    ],
    sdk_version: "current",
}

android_library {
    name: "SimpleImeTestingLib",
    srcs: [
        "src/com/android/apps/inputmethod/simpleime/testing/*.java",
    ],
    sdk_version: "current",
}
Loading