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; + } +}