From 640b2b793f67c909f746385d036511bc4ad40125 Mon Sep 17 00:00:00 2001 From: Lan Wei Date: Mon, 21 Nov 2022 14:57:43 +0800 Subject: [PATCH] Add test app SimpleTestIme to test InputMethodService Add a test app SimpleTestIme under services/tests/InputMethodSystemServerTests/test-apps/ which provides the basic functionality of a keyboard. Using it as the target package of instrumentation test, which means we have access to all the class instances running in it under the tests, including InputMethodService. In this way we can get a real InputMethodService instance, then manipulate and test the states of it. Add simple test cases to verify the behavior of some public methods like: * onStartInput()/onFinishInput() * onStartInputView()/onFinishInputView() * requestShowSelf()/requestHideSelf() * onEvaluateFullscreenMode()/isFullScreenMode() Bug: 240359838 Test: com.android.inputmethodservice.InputMethodServiceTest Change-Id: I84f1a1b25eef4a757d8da27807285e08417458b4 --- .../InputMethodSystemServerTests/Android.bp | 51 +++- .../inputmethodservice/AndroidManifest.xml | 43 +++ .../inputmethodservice/AndroidTest.xml | 41 +++ .../InputMethodServiceTest.java | 276 ++++++++++++++++++ .../test-apps/SimpleTestIme/Android.bp | 62 ++++ .../SimpleTestIme/AndroidManifest.xml | 54 ++++ .../SimpleTestIme/res/drawable/key_border.xml | 30 ++ .../SimpleTestIme/res/layout/input_view.xml | 22 ++ .../res/layout/qwerty_10_9_9.xml | 200 +++++++++++++ .../SimpleTestIme/res/values/dimens.xml | 24 ++ .../SimpleTestIme/res/values/strings.xml | 20 ++ .../SimpleTestIme/res/values/styles.xml | 66 +++++ .../SimpleTestIme/res/xml/method.xml | 23 ++ .../simpleime/KeyCodeConstants.java | 68 +++++ .../simpleime/SimpleInputMethodService.java | 67 +++++ .../inputmethod/simpleime/SimpleKeyboard.java | 126 ++++++++ .../ims/InputMethodServiceWrapper.java | 102 +++++++ .../simpleime/testing/TestActivity.java | 105 +++++++ 18 files changed, 1379 insertions(+), 1 deletion(-) create mode 100644 services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml create mode 100644 services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidTest.xml create mode 100644 services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java create mode 100644 services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp create mode 100644 services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml create mode 100644 services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/drawable/key_border.xml create mode 100644 services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/layout/input_view.xml create mode 100644 services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/layout/qwerty_10_9_9.xml create mode 100644 services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/dimens.xml create mode 100644 services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/strings.xml create mode 100644 services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/styles.xml create mode 100644 services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/xml/method.xml create mode 100644 services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/KeyCodeConstants.java create mode 100644 services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleInputMethodService.java create mode 100644 services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleKeyboard.java create mode 100644 services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java create mode 100644 services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/testing/TestActivity.java diff --git a/services/tests/InputMethodSystemServerTests/Android.bp b/services/tests/InputMethodSystemServerTests/Android.bp index 939fb6a9d1700..70a5c3f961c4f 100644 --- a/services/tests/InputMethodSystemServerTests/Android.bp +++ b/services/tests/InputMethodSystemServerTests/Android.bp @@ -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, + }, +} diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml new file mode 100644 index 0000000000000..0104f7142bea6 --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidTest.xml b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidTest.xml new file mode 100644 index 0000000000000..6c24d6d6e5a6e --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidTest.xml @@ -0,0 +1,41 @@ + + + + diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java new file mode 100644 index 0000000000000..16a98454d135b --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java @@ -0,0 +1,276 @@ +/* + * 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(); + } +} diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp new file mode 100644 index 0000000000000..ef50476c2928a --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp @@ -0,0 +1,62 @@ +// 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", +} diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml new file mode 100644 index 0000000000000..802caf132db58 --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/drawable/key_border.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/drawable/key_border.xml new file mode 100644 index 0000000000000..dbfcc30a7f4eb --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/drawable/key_border.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/layout/input_view.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/layout/input_view.xml new file mode 100644 index 0000000000000..f2292701e7959 --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/layout/input_view.xml @@ -0,0 +1,22 @@ + + + + \ No newline at end of file diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/layout/qwerty_10_9_9.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/layout/qwerty_10_9_9.xml new file mode 100644 index 0000000000000..ee94ea9eee1e9 --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/layout/qwerty_10_9_9.xml @@ -0,0 +1,200 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/dimens.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/dimens.xml new file mode 100644 index 0000000000000..1a4959e14a1fc --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/dimens.xml @@ -0,0 +1,24 @@ + + + + + 24dp + 14dp + + 40dp + 50dp + \ No newline at end of file diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/strings.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/strings.xml new file mode 100644 index 0000000000000..11377fa02dc9c --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/strings.xml @@ -0,0 +1,20 @@ + + + + + Fake IME + \ No newline at end of file diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/styles.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/styles.xml new file mode 100644 index 0000000000000..83f7bc3170668 --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/styles.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/xml/method.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/xml/method.xml new file mode 100644 index 0000000000000..872b0688814ad --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/xml/method.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/KeyCodeConstants.java b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/KeyCodeConstants.java new file mode 100644 index 0000000000000..990fa24aad220 --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/KeyCodeConstants.java @@ -0,0 +1,68 @@ +/* + * 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.apps.inputmethod.simpleime; + +import android.view.KeyEvent; + +import java.util.HashMap; + +/** Holder of key codes and their name. */ +public final class KeyCodeConstants { + private KeyCodeConstants() {} + + static final HashMap KEY_NAME_TO_CODE_MAP = new HashMap<>(); + + static { + KEY_NAME_TO_CODE_MAP.put("KEYCODE_A", KeyEvent.KEYCODE_A); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_B", KeyEvent.KEYCODE_B); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_C", KeyEvent.KEYCODE_C); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_D", KeyEvent.KEYCODE_D); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_E", KeyEvent.KEYCODE_E); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_F", KeyEvent.KEYCODE_F); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_G", KeyEvent.KEYCODE_G); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_H", KeyEvent.KEYCODE_H); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_I", KeyEvent.KEYCODE_I); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_J", KeyEvent.KEYCODE_J); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_K", KeyEvent.KEYCODE_K); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_L", KeyEvent.KEYCODE_L); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_M", KeyEvent.KEYCODE_M); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_N", KeyEvent.KEYCODE_N); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_O", KeyEvent.KEYCODE_O); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_P", KeyEvent.KEYCODE_P); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_Q", KeyEvent.KEYCODE_Q); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_R", KeyEvent.KEYCODE_R); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_S", KeyEvent.KEYCODE_S); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_T", KeyEvent.KEYCODE_T); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_U", KeyEvent.KEYCODE_U); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_V", KeyEvent.KEYCODE_V); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_W", KeyEvent.KEYCODE_W); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_X", KeyEvent.KEYCODE_X); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_Y", KeyEvent.KEYCODE_Y); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_Z", KeyEvent.KEYCODE_Z); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_SHIFT", KeyEvent.KEYCODE_SHIFT_LEFT); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_DEL", KeyEvent.KEYCODE_DEL); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_SPACE", KeyEvent.KEYCODE_SPACE); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_ENTER", KeyEvent.KEYCODE_ENTER); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_COMMA", KeyEvent.KEYCODE_COMMA); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_PERIOD", KeyEvent.KEYCODE_PERIOD); + KEY_NAME_TO_CODE_MAP.put("KEYCODE_TAB", KeyEvent.KEYCODE_TAB); + } + + public static boolean isAlphaKeyCode(int keyCode) { + return keyCode >= KeyEvent.KEYCODE_A && keyCode <= KeyEvent.KEYCODE_Z; + } +} diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleInputMethodService.java b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleInputMethodService.java new file mode 100644 index 0000000000000..48942a31658fb --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleInputMethodService.java @@ -0,0 +1,67 @@ +/* + * 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.apps.inputmethod.simpleime; + +import android.inputmethodservice.InputMethodService; +import android.os.SystemClock; +import android.util.Log; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.widget.FrameLayout; + +import com.android.apps.inputmethod.simpleime.ims.InputMethodServiceWrapper; + +/** The {@link InputMethodService} implementation for SimpeTestIme app. */ +public class SimpleInputMethodService extends InputMethodServiceWrapper { + private static final String TAG = "SimpleIMS"; + + private FrameLayout mInputView; + + @Override + public View onCreateInputView() { + Log.i(TAG, "onCreateInputView()"); + mInputView = (FrameLayout) LayoutInflater.from(this).inflate(R.layout.input_view, null); + return mInputView; + } + + @Override + public void onStartInputView(EditorInfo info, boolean restarting) { + super.onStartInputView(info, restarting); + mInputView.removeAllViews(); + SimpleKeyboard keyboard = new SimpleKeyboard(this, R.layout.qwerty_10_9_9); + mInputView.addView(keyboard.inflateKeyboardView(LayoutInflater.from(this), mInputView)); + } + + void handle(String data, int keyboardState) { + InputConnection inputConnection = getCurrentInputConnection(); + Integer keyCode = KeyCodeConstants.KEY_NAME_TO_CODE_MAP.get(data); + Log.v(TAG, "keyCode: " + keyCode); + if (keyCode != null) { + inputConnection.sendKeyEvent( + new KeyEvent( + SystemClock.uptimeMillis(), + SystemClock.uptimeMillis(), + KeyEvent.ACTION_DOWN, + keyCode, + 0, + KeyCodeConstants.isAlphaKeyCode(keyCode) ? keyboardState : 0)); + } + } +} diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleKeyboard.java b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleKeyboard.java new file mode 100644 index 0000000000000..b16ec9ebd7189 --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleKeyboard.java @@ -0,0 +1,126 @@ +/* + * 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.apps.inputmethod.simpleime; + +import android.text.TextUtils; +import android.util.Log; +import android.util.SparseArray; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +/** Controls the visible virtual keyboard view. */ +final class SimpleKeyboard { + private static final String TAG = "SimpleKeyboard"; + + private static final int[] SOFT_KEY_IDS = + new int[] { + R.id.key_pos_0_0, + R.id.key_pos_0_1, + R.id.key_pos_0_2, + R.id.key_pos_0_3, + R.id.key_pos_0_4, + R.id.key_pos_0_5, + R.id.key_pos_0_6, + R.id.key_pos_0_7, + R.id.key_pos_0_8, + R.id.key_pos_0_9, + R.id.key_pos_1_0, + R.id.key_pos_1_1, + R.id.key_pos_1_2, + R.id.key_pos_1_3, + R.id.key_pos_1_4, + R.id.key_pos_1_5, + R.id.key_pos_1_6, + R.id.key_pos_1_7, + R.id.key_pos_1_8, + R.id.key_pos_2_0, + R.id.key_pos_2_1, + R.id.key_pos_2_2, + R.id.key_pos_2_3, + R.id.key_pos_2_4, + R.id.key_pos_2_5, + R.id.key_pos_2_6, + R.id.key_pos_shift, + R.id.key_pos_del, + R.id.key_pos_symbol, + R.id.key_pos_comma, + R.id.key_pos_space, + R.id.key_pos_period, + R.id.key_pos_enter, + }; + + private final SimpleInputMethodService mSimpleInputMethodService; + private final int mViewResId; + private final SparseArray mSoftKeyViews = new SparseArray<>(); + private View mKeyboardView; + private int mKeyboardState; + + SimpleKeyboard(SimpleInputMethodService simpleInputMethodService, int viewResId) { + this.mSimpleInputMethodService = simpleInputMethodService; + this.mViewResId = viewResId; + this.mKeyboardState = 0; + } + + View inflateKeyboardView(LayoutInflater inflater, ViewGroup inputView) { + mKeyboardView = inflater.inflate(mViewResId, inputView, false); + mapSoftKeys(); + return mKeyboardView; + } + + private void mapSoftKeys() { + for (int id : SOFT_KEY_IDS) { + TextView softKeyView = mKeyboardView.findViewById(id); + mSoftKeyViews.put(id, softKeyView); + String tagData = softKeyView.getTag() != null ? softKeyView.getTag().toString() : null; + softKeyView.setOnClickListener(v -> handle(tagData)); + } + } + + private void handle(String data) { + Log.i(TAG, "handle(): " + data); + if (TextUtils.isEmpty(data)) { + return; + } + if ("KEYCODE_SHIFT".equals(data)) { + handleShift(); + return; + } + + mSimpleInputMethodService.handle(data, mKeyboardState); + } + + private void handleShift() { + mKeyboardState = toggleShiftState(mKeyboardState); + Log.v(TAG, "currentKeyboardState: " + mKeyboardState); + boolean isShiftOn = isShiftOn(mKeyboardState); + for (int i = 0; i < mSoftKeyViews.size(); i++) { + TextView softKeyView = mSoftKeyViews.valueAt(i); + softKeyView.setAllCaps(isShiftOn); + } + } + + private static boolean isShiftOn(int state) { + return (state & KeyEvent.META_SHIFT_ON) == KeyEvent.META_SHIFT_ON; + } + + private static int toggleShiftState(int state) { + return state ^ KeyEvent.META_SHIFT_ON; + } +} diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java new file mode 100644 index 0000000000000..b706a654e3a80 --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java @@ -0,0 +1,102 @@ +/* + * 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.apps.inputmethod.simpleime.ims; + +import android.content.res.Configuration; +import android.inputmethodservice.InputMethodService; +import android.util.Log; +import android.view.inputmethod.EditorInfo; + +import java.util.concurrent.CountDownLatch; + +/** Wrapper of {@link InputMethodService} to expose interfaces for testing purpose. */ +public class InputMethodServiceWrapper extends InputMethodService { + private static final String TAG = "InputMethodServiceWrapper"; + + private static InputMethodServiceWrapper sInputMethodServiceWrapper; + + public static InputMethodServiceWrapper getInputMethodServiceWrapperForTesting() { + return sInputMethodServiceWrapper; + } + + private boolean mInputViewStarted; + private CountDownLatch mCountDownLatchForTesting; + + public boolean getCurrentInputViewStarted() { + return mInputViewStarted; + } + + public void setCountDownLatchForTesting(CountDownLatch countDownLatchForTesting) { + mCountDownLatchForTesting = countDownLatchForTesting; + } + + @Override + public void onCreate() { + Log.i(TAG, "onCreate()"); + super.onCreate(); + sInputMethodServiceWrapper = this; + } + + @Override + public void onStartInput(EditorInfo info, boolean restarting) { + Log.i(TAG, "onStartInput() editor=" + info + ", restarting=" + restarting); + super.onStartInput(info, restarting); + } + + @Override + public void onStartInputView(EditorInfo info, boolean restarting) { + Log.i(TAG, "onStartInputView() editor=" + info + ", restarting=" + restarting); + super.onStartInputView(info, restarting); + mInputViewStarted = true; + if (mCountDownLatchForTesting != null) { + mCountDownLatchForTesting.countDown(); + } + } + + @Override + public void onFinishInput() { + Log.i(TAG, "onFinishInput()"); + super.onFinishInput(); + } + + @Override + public void onFinishInputView(boolean finishingInput) { + Log.i(TAG, "onFinishInputView()"); + super.onFinishInputView(finishingInput); + mInputViewStarted = false; + + if (mCountDownLatchForTesting != null) { + mCountDownLatchForTesting.countDown(); + } + } + + @Override + public void requestHideSelf(int flags) { + Log.i(TAG, "requestHideSelf() " + flags); + super.requestHideSelf(flags); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + Log.i(TAG, "onConfigurationChanged() " + newConfig); + super.onConfigurationChanged(newConfig); + + if (mCountDownLatchForTesting != null) { + mCountDownLatchForTesting.countDown(); + } + } +} diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/testing/TestActivity.java b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/testing/TestActivity.java new file mode 100644 index 0000000000000..0eec7e629d870 --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/testing/TestActivity.java @@ -0,0 +1,105 @@ +/* + * 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.apps.inputmethod.simpleime.testing; + +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +import android.app.Activity; +import android.app.Instrumentation; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.WindowInsets; +import android.view.WindowInsetsController; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.LinearLayout; + +/** + * A special activity for testing purpose. + * + *

This is used when the instruments package is SimpleTestIme, as the Intent needs to be started + * in the instruments package. More details see {@link + * Instrumentation#startActivitySync(Intent)}. + */ +public class TestActivity extends Activity { + private static final String TAG = "TestActivity"; + + /** + * Start a new test activity with an editor and wait for it to begin running before returning. + * + * @param instrumentation application instrumentation + * @return the newly started activity + */ + public static TestActivity start(Instrumentation instrumentation) { + Intent intent = + new Intent() + .setAction(Intent.ACTION_MAIN) + .setClass(instrumentation.getTargetContext(), TestActivity.class) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + return (TestActivity) instrumentation.startActivitySync(intent); + } + + public EditText mEditText; + + @Override + protected void onCreate(Bundle savedInstanceState) { + getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); + LinearLayout rootView = new LinearLayout(this); + mEditText = new EditText(this); + mEditText.setContentDescription("Input box"); + rootView.addView(mEditText, new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)); + setContentView(rootView); + mEditText.requestFocus(); + super.onCreate(savedInstanceState); + } + + /** Shows soft keyboard via InputMethodManager. */ + public boolean showImeWithInputMethodManager(int flags) { + InputMethodManager imm = getSystemService(InputMethodManager.class); + boolean result = imm.showSoftInput(mEditText, flags); + Log.i(TAG, "hideIme() via InputMethodManager, result=" + result); + return result; + } + + /** Shows soft keyboard via WindowInsetsController. */ + public boolean showImeWithWindowInsetsController() { + WindowInsetsController windowInsetsController = mEditText.getWindowInsetsController(); + windowInsetsController.show(WindowInsets.Type.ime()); + Log.i(TAG, "showIme() via WindowInsetsController"); + return true; + } + + /** Hides soft keyboard via InputMethodManager. */ + public boolean hideImeWithInputMethodManager(int flags) { + InputMethodManager imm = getSystemService(InputMethodManager.class); + boolean result = imm.hideSoftInputFromWindow(mEditText.getWindowToken(), flags); + Log.i(TAG, "hideIme() via InputMethodManager, result=" + result); + return result; + } + + /** Hides soft keyboard via WindowInsetsController. */ + public boolean hideImeWithWindowInsetsController() { + WindowInsetsController windowInsetsController = mEditText.getWindowInsetsController(); + windowInsetsController.hide(WindowInsets.Type.ime()); + Log.i(TAG, "hideIme() via WindowInsetsController"); + return true; + } +}