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