Add unit tests for InputMethodManagerService

Injecting and mocking dependencies to InputMethodManagerService to make
it unit testable.

Add InputMethodManagerServiceTestBase base class to help unit test IMMS.

Add the first batch of unit tests in InputMethodManagerServiceWindowGainedFocusTest to cover the behavior of IMMS#startInputOrWindowGainedFocus(), which should help us catch any regressions caused by future changes to the code.

Bug: 240359838
Test: atest com.android.server.inputmethod.InputMethodManagerServiceWindowGainedFocusTest
Change-Id: Id61087f1e176738910f5acfe912f8682f727255c
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 76495b1..12c5aa8 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -144,6 +144,7 @@
 import android.window.ImeOnBackInvokedDispatcher;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.content.PackageMonitor;
 import com.android.internal.infra.AndroidFuture;
 import com.android.internal.inputmethod.DirectBootAwareness;
@@ -1592,8 +1593,13 @@
         private final InputMethodManagerService mService;
 
         public Lifecycle(Context context) {
+            this(context, new InputMethodManagerService(context));
+        }
+
+        public Lifecycle(
+                Context context, @NonNull InputMethodManagerService inputMethodManagerService) {
             super(context);
-            mService = new InputMethodManagerService(context);
+            mService = inputMethodManagerService;
         }
 
         @Override
@@ -1668,12 +1674,25 @@
     }
 
     public InputMethodManagerService(Context context) {
+        this(context, null, null);
+    }
+
+    @VisibleForTesting
+    InputMethodManagerService(
+            Context context,
+            @Nullable ServiceThread serviceThreadForTesting,
+            @Nullable InputMethodBindingController bindingControllerForTesting) {
         mContext = context;
         mRes = context.getResources();
         // TODO(b/196206770): Disallow I/O on this thread. Currently it's needed for loading
         // additional subtypes in switchUserOnHandlerLocked().
-        final ServiceThread thread = new ServiceThread(
-                HANDLER_THREAD_NAME, Process.THREAD_PRIORITY_FOREGROUND, true /* allowIo */);
+        final ServiceThread thread =
+                serviceThreadForTesting != null
+                        ? serviceThreadForTesting
+                        : new ServiceThread(
+                                HANDLER_THREAD_NAME,
+                                Process.THREAD_PRIORITY_FOREGROUND,
+                                true /* allowIo */);
         thread.start();
         mHandler = Handler.createAsync(thread.getLooper(), this);
         // Note: SettingsObserver doesn't register observers in its constructor.
@@ -1701,10 +1720,13 @@
 
         updateCurrentProfileIds();
         AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, userId);
-        mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked(
-                mSettings, context);
+        mSwitchingController =
+                InputMethodSubtypeSwitchingController.createInstanceLocked(mSettings, context);
         mMenuController = new InputMethodMenuController(this);
-        mBindingController = new InputMethodBindingController(this);
+        mBindingController =
+                bindingControllerForTesting != null
+                        ? bindingControllerForTesting
+                        : new InputMethodBindingController(this);
         mAutofillController = new AutofillSuggestionsController(this);
         mPreventImeStartupUnlessTextEditor = mRes.getBoolean(
                 com.android.internal.R.bool.config_preventImeStartupUnlessTextEditor);
@@ -3673,6 +3695,7 @@
         // UI for input.
         if (isTextEditor && editorInfo != null
                 && shouldRestoreImeVisibility(windowToken, softInputMode)) {
+            if (DEBUG) Slog.v(TAG, "Will show input to restore visibility");
             res = startInputUncheckedLocked(cs, inputContext, remoteAccessibilityInputConnection,
                     editorInfo, startInputFlags, startInputReason, unverifiedTargetSdkVersion,
                     imeDispatcher);
@@ -3719,11 +3742,17 @@
                                 imeDispatcher);
                         didStart = true;
                     }
-                    showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null,
+                    showCurrentInputLocked(
+                            windowToken,
+                            InputMethodManager.SHOW_IMPLICIT,
+                            null,
                             SoftInputShowHideReason.SHOW_AUTO_EDITOR_FORWARD_NAV);
                 }
                 break;
             case LayoutParams.SOFT_INPUT_STATE_UNCHANGED:
+                if (DEBUG) {
+                    Slog.v(TAG, "Window asks to keep the input in whatever state it was last in");
+                }
                 // Do nothing.
                 break;
             case LayoutParams.SOFT_INPUT_STATE_HIDDEN:
@@ -3794,6 +3823,7 @@
                     // To maintain compatibility, we are now hiding the IME when we don't have
                     // an editor upon refocusing a window.
                     if (startInputByWinGainedFocus) {
+                        if (DEBUG) Slog.v(TAG, "Same window without editor will hide input");
                         hideCurrentInputLocked(mCurFocusedWindow, 0, null,
                                 SoftInputShowHideReason.HIDE_SAME_WINDOW_FOCUSED_WITHOUT_EDITOR);
                     }
@@ -3807,6 +3837,7 @@
                     // 1) SOFT_INPUT_STATE_UNCHANGED state without an editor
                     // 2) SOFT_INPUT_STATE_VISIBLE state without an editor
                     // 3) SOFT_INPUT_STATE_ALWAYS_VISIBLE state without an editor
+                    if (DEBUG) Slog.v(TAG, "Window without editor will hide input");
                     hideCurrentInputLocked(mCurFocusedWindow, 0, null,
                             SoftInputShowHideReason.HIDE_WINDOW_GAINED_FOCUS_WITHOUT_EDITOR);
                 }
diff --git a/services/tests/InputMethodSystemServerTests/Android.bp b/services/tests/InputMethodSystemServerTests/Android.bp
new file mode 100644
index 0000000..939fb6a
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/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 {
+    name: "FrameworksInputMethodSystemServerTests",
+    defaults: [
+        "modules-utils-testable-device-config-defaults",
+    ],
+
+    srcs: [
+        "src/**/*.java",
+    ],
+
+    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",
+    ],
+
+    libs: [
+        "android.test.mock",
+        "android.test.base",
+        "android.test.runner",
+    ],
+
+    certificate: "platform",
+    platform_apis: true,
+    test_suites: ["device-tests"],
+
+    optimize: {
+        enabled: false,
+    },
+}
diff --git a/services/tests/InputMethodSystemServerTests/AndroidManifest.xml b/services/tests/InputMethodSystemServerTests/AndroidManifest.xml
new file mode 100644
index 0000000..12e7cfc
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/AndroidManifest.xml
@@ -0,0 +1,42 @@
+<?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.frameworks.inputmethodtests">
+
+    <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:testOnly="true"
+                 android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.frameworks.inputmethodtests"
+        android:label="Frameworks InputMethod System Service Tests" />
+
+</manifest>
diff --git a/services/tests/InputMethodSystemServerTests/AndroidTest.xml b/services/tests/InputMethodSystemServerTests/AndroidTest.xml
new file mode 100644
index 0000000..92be780
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/AndroidTest.xml
@@ -0,0 +1,39 @@
+<?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 InputMethod System Services 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="FrameworksInputMethodSystemServerTests.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="FrameworksInputMethodSystemServerTests" />
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.frameworks.inputmethodtests" />
+        <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/FrameworksInputMethodSystemServerTests/" />
+        <option name="collect-on-run-ended-only" value="true" />
+    </metrics_collector>
+</configuration>
diff --git a/services/tests/InputMethodSystemServerTests/OWNERS b/services/tests/InputMethodSystemServerTests/OWNERS
new file mode 100644
index 0000000..1f2c036
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/OWNERS
@@ -0,0 +1 @@
+include /services/core/java/com/android/server/inputmethod/OWNERS
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
new file mode 100644
index 0000000..3fbc400
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
@@ -0,0 +1,259 @@
+/*
+ * 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.server.inputmethod;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManagerInternal;
+import android.content.Context;
+import android.content.pm.PackageManagerInternal;
+import android.hardware.display.DisplayManagerInternal;
+import android.hardware.input.IInputManager;
+import android.hardware.input.InputManager;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.view.inputmethod.EditorInfo;
+import android.window.ImeOnBackInvokedDispatcher;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.internal.compat.IPlatformCompat;
+import com.android.internal.inputmethod.IInputMethod;
+import com.android.internal.inputmethod.IInputMethodClient;
+import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection;
+import com.android.internal.inputmethod.IRemoteInputConnection;
+import com.android.internal.inputmethod.InputBindResult;
+import com.android.internal.view.IInputMethodManager;
+import com.android.server.LocalServices;
+import com.android.server.ServiceThread;
+import com.android.server.SystemServerInitThreadPool;
+import com.android.server.SystemService;
+import com.android.server.input.InputManagerInternal;
+import com.android.server.pm.UserManagerInternal;
+import com.android.server.wm.WindowManagerInternal;
+
+import org.junit.After;
+import org.junit.Before;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+/** Base class for testing {@link InputMethodManagerService}. */
+public class InputMethodManagerServiceTestBase {
+    protected static final String TEST_SELECTED_IME_ID = "test.ime";
+    protected static final String TEST_EDITOR_PKG_NAME = "test.editor";
+    protected static final String TEST_FOCUSED_WINDOW_NAME = "test.editor/activity";
+    protected static final WindowManagerInternal.ImeTargetInfo TEST_IME_TARGET_INFO =
+            new WindowManagerInternal.ImeTargetInfo(
+                    TEST_FOCUSED_WINDOW_NAME,
+                    TEST_FOCUSED_WINDOW_NAME,
+                    TEST_FOCUSED_WINDOW_NAME,
+                    TEST_FOCUSED_WINDOW_NAME,
+                    TEST_FOCUSED_WINDOW_NAME);
+    protected static final InputBindResult SUCCESS_WAITING_IME_BINDING_RESULT =
+            new InputBindResult(
+                    InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING,
+                    null,
+                    null,
+                    null,
+                    "0",
+                    0,
+                    null,
+                    false);
+
+    @Mock protected WindowManagerInternal mMockWindowManagerInternal;
+    @Mock protected ActivityManagerInternal mMockActivityManagerInternal;
+    @Mock protected PackageManagerInternal mMockPackageManagerInternal;
+    @Mock protected InputManagerInternal mMockInputManagerInternal;
+    @Mock protected DisplayManagerInternal mMockDisplayManagerInternal;
+    @Mock protected UserManagerInternal mMockUserManagerInternal;
+    @Mock protected InputMethodBindingController mMockInputMethodBindingController;
+    @Mock protected IInputMethodClient mMockInputMethodClient;
+    @Mock protected IBinder mWindowToken;
+    @Mock protected IRemoteInputConnection mMockRemoteInputConnection;
+    @Mock protected IRemoteAccessibilityInputConnection mMockRemoteAccessibilityInputConnection;
+    @Mock protected ImeOnBackInvokedDispatcher mMockImeOnBackInvokedDispatcher;
+    @Mock protected IInputMethodManager.Stub mMockIInputMethodManager;
+    @Mock protected IPlatformCompat.Stub mMockIPlatformCompat;
+    @Mock protected IInputMethod mMockInputMethod;
+    @Mock protected IBinder mMockInputMethodBinder;
+    @Mock protected IInputManager mMockIInputManager;
+
+    protected Context mContext;
+    protected MockitoSession mMockingSession;
+    protected int mTargetSdkVersion;
+    protected int mCallingUserId;
+    protected EditorInfo mEditorInfo;
+    protected IInputMethodInvoker mMockInputMethodInvoker;
+    protected InputMethodManagerService mInputMethodManagerService;
+    protected ServiceThread mServiceThread;
+
+    @Before
+    public void setUp() throws RemoteException {
+        mMockingSession =
+                mockitoSession()
+                        .initMocks(this)
+                        .strictness(Strictness.LENIENT)
+                        .mockStatic(LocalServices.class)
+                        .mockStatic(ServiceManager.class)
+                        .mockStatic(SystemServerInitThreadPool.class)
+                        .startMocking();
+
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        spyOn(mContext);
+
+        mTargetSdkVersion = mContext.getApplicationInfo().targetSdkVersion;
+        mCallingUserId = UserHandle.getCallingUserId();
+        mEditorInfo = new EditorInfo();
+        mEditorInfo.packageName = TEST_EDITOR_PKG_NAME;
+
+        // Injecting and mocking local services.
+        doReturn(mMockWindowManagerInternal)
+                .when(() -> LocalServices.getService(WindowManagerInternal.class));
+        doReturn(mMockActivityManagerInternal)
+                .when(() -> LocalServices.getService(ActivityManagerInternal.class));
+        doReturn(mMockPackageManagerInternal)
+                .when(() -> LocalServices.getService(PackageManagerInternal.class));
+        doReturn(mMockInputManagerInternal)
+                .when(() -> LocalServices.getService(InputManagerInternal.class));
+        doReturn(mMockDisplayManagerInternal)
+                .when(() -> LocalServices.getService(DisplayManagerInternal.class));
+        doReturn(mMockUserManagerInternal)
+                .when(() -> LocalServices.getService(UserManagerInternal.class));
+        doReturn(mMockIInputMethodManager)
+                .when(() -> ServiceManager.getServiceOrThrow(Context.INPUT_METHOD_SERVICE));
+        doReturn(mMockIPlatformCompat)
+                .when(() -> ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
+
+        // Stubbing out context related methods to avoid the system holding strong references to
+        // InputMethodManagerService.
+        doNothing().when(mContext).enforceCallingPermission(anyString(), anyString());
+        doNothing().when(mContext).sendBroadcastAsUser(any(), any());
+        doReturn(null).when(mContext).registerReceiver(any(), any());
+        doReturn(null)
+                .when(mContext)
+                .registerReceiverAsUser(any(), any(), any(), anyString(), any(), anyInt());
+
+        // Injecting and mocked InputMethodBindingController and InputMethod.
+        mMockInputMethodInvoker = IInputMethodInvoker.create(mMockInputMethod);
+        InputManager.resetInstance(mMockIInputManager);
+        synchronized (ImfLock.class) {
+            when(mMockInputMethodBindingController.getCurMethod())
+                    .thenReturn(mMockInputMethodInvoker);
+            when(mMockInputMethodBindingController.bindCurrentMethod())
+                    .thenReturn(SUCCESS_WAITING_IME_BINDING_RESULT);
+            doNothing().when(mMockInputMethodBindingController).unbindCurrentMethod();
+            when(mMockInputMethodBindingController.getSelectedMethodId())
+                    .thenReturn(TEST_SELECTED_IME_ID);
+        }
+
+        // Shuffling around all other initialization to make the test runnable.
+        when(mMockIInputManager.getInputDeviceIds()).thenReturn(new int[0]);
+        when(mMockIInputMethodManager.isImeTraceEnabled()).thenReturn(false);
+        when(mMockIPlatformCompat.isChangeEnabledByUid(anyLong(), anyInt())).thenReturn(true);
+        when(mMockUserManagerInternal.isUserRunning(anyInt())).thenReturn(true);
+        when(mMockUserManagerInternal.getProfileIds(anyInt(), anyBoolean()))
+                .thenReturn(new int[] {0});
+        when(mMockActivityManagerInternal.isSystemReady()).thenReturn(true);
+        when(mMockPackageManagerInternal.getPackageUid(anyString(), anyLong(), anyInt()))
+                .thenReturn(Binder.getCallingUid());
+        when(mMockWindowManagerInternal.onToggleImeRequested(anyBoolean(), any(), any(), anyInt()))
+                .thenReturn(TEST_IME_TARGET_INFO);
+        when(mMockInputMethodClient.asBinder()).thenReturn(mMockInputMethodBinder);
+
+        // Used by lazy initializing draw IMS nav bar at InputMethodManagerService#systemRunning(),
+        // which is ok to be mocked out for now.
+        doReturn(null).when(() -> SystemServerInitThreadPool.submit(any(), anyString()));
+
+        mServiceThread =
+                new ServiceThread(
+                        "TestServiceThread",
+                        Process.THREAD_PRIORITY_FOREGROUND, /* allowIo */
+                        false);
+        mInputMethodManagerService =
+                new InputMethodManagerService(
+                        mContext, mServiceThread, mMockInputMethodBindingController);
+
+        // Start a InputMethodManagerService.Lifecycle to publish and manage the lifecycle of
+        // InputMethodManagerService, which is closer to the real situation.
+        InputMethodManagerService.Lifecycle lifecycle =
+                new InputMethodManagerService.Lifecycle(mContext, mInputMethodManagerService);
+
+        // Public local InputMethodManagerService.
+        lifecycle.onStart();
+        try {
+            // After this boot phase, services can broadcast Intents.
+            lifecycle.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY);
+        } catch (SecurityException e) {
+            // Security exception to permission denial is expected in test, mocking out to ensure
+            // InputMethodManagerService as system ready state.
+            if (!e.getMessage().contains("Permission Denial: not allowed to send broadcast")) {
+                throw e;
+            }
+        }
+
+        // Call InputMethodManagerService#addClient() as a preparation to start interacting with it.
+        mInputMethodManagerService.addClient(mMockInputMethodClient, mMockRemoteInputConnection, 0);
+    }
+
+    @After
+    public void tearDown() {
+        if (mServiceThread != null) {
+            mServiceThread.quitSafely();
+        }
+
+        if (mMockingSession != null) {
+            mMockingSession.finishMocking();
+        }
+    }
+
+    protected void verifyShowSoftInput(boolean setVisible, boolean showSoftInput)
+            throws RemoteException {
+        synchronized (ImfLock.class) {
+            verify(mMockInputMethodBindingController, times(setVisible ? 1 : 0))
+                    .setCurrentMethodVisible();
+        }
+        verify(mMockInputMethod, times(showSoftInput ? 1 : 0))
+                .showSoftInput(any(), anyInt(), any());
+    }
+
+    protected void verifyHideSoftInput(boolean setNotVisible, boolean hideSoftInput)
+            throws RemoteException {
+        synchronized (ImfLock.class) {
+            verify(mMockInputMethodBindingController, times(setNotVisible ? 1 : 0))
+                    .setCurrentMethodNotVisible();
+        }
+        verify(mMockInputMethod, times(hideSoftInput ? 1 : 0))
+                .hideSoftInput(any(), anyInt(), any());
+    }
+}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
new file mode 100644
index 0000000..ffa2729
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
@@ -0,0 +1,266 @@
+/*
+ * 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.server.inputmethod;
+
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.when;
+
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.inputmethod.EditorInfo;
+import android.window.ImeOnBackInvokedDispatcher;
+
+import com.android.internal.inputmethod.IInputMethodClient;
+import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection;
+import com.android.internal.inputmethod.IRemoteInputConnection;
+import com.android.internal.inputmethod.InputBindResult;
+import com.android.internal.inputmethod.InputMethodDebug;
+import com.android.internal.inputmethod.StartInputFlags;
+import com.android.internal.inputmethod.StartInputReason;
+import com.android.server.wm.WindowManagerInternal;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test the behavior of {@link InputMethodManagerService#startInputOrWindowGainedFocus(int,
+ * IInputMethodClient, IBinder, int, int, int, EditorInfo, IRemoteInputConnection,
+ * IRemoteAccessibilityInputConnection, int, int, ImeOnBackInvokedDispatcher)}.
+ */
+@RunWith(Parameterized.class)
+public class InputMethodManagerServiceWindowGainedFocusTest
+        extends InputMethodManagerServiceTestBase {
+    private static final String TAG = "IMMSWindowGainedFocusTest";
+
+    private static final int[] SOFT_INPUT_STATE_FLAGS =
+            new int[] {
+                SOFT_INPUT_STATE_UNSPECIFIED,
+                SOFT_INPUT_STATE_UNCHANGED,
+                SOFT_INPUT_STATE_HIDDEN,
+                SOFT_INPUT_STATE_ALWAYS_HIDDEN,
+                SOFT_INPUT_STATE_VISIBLE,
+                SOFT_INPUT_STATE_ALWAYS_VISIBLE
+            };
+    private static final int[] SOFT_INPUT_ADJUST_FLAGS =
+            new int[] {
+                SOFT_INPUT_ADJUST_UNSPECIFIED,
+                SOFT_INPUT_ADJUST_RESIZE,
+                SOFT_INPUT_ADJUST_PAN,
+                SOFT_INPUT_ADJUST_NOTHING
+            };
+    private static final int DEFAULT_SOFT_INPUT_FLAG =
+            StartInputFlags.VIEW_HAS_FOCUS | StartInputFlags.IS_TEXT_EDITOR;
+
+    @Parameterized.Parameters(name = "softInputState={0}, softInputAdjustment={1}")
+    public static List<Object[]> softInputModeConfigs() {
+        ArrayList<Object[]> params = new ArrayList<>();
+        for (int softInputState : SOFT_INPUT_STATE_FLAGS) {
+            for (int softInputAdjust : SOFT_INPUT_ADJUST_FLAGS) {
+                params.add(new Object[] {softInputState, softInputAdjust});
+            }
+        }
+        return params;
+    }
+
+    private final int mSoftInputState;
+    private final int mSoftInputAdjustment;
+
+    public InputMethodManagerServiceWindowGainedFocusTest(
+            int softInputState, int softInputAdjustment) {
+        mSoftInputState = softInputState;
+        mSoftInputAdjustment = softInputAdjustment;
+    }
+
+    @Test
+    public void startInputOrWindowGainedFocus_forwardNavigation() throws RemoteException {
+        mockHasImeFocusAndRestoreImeVisibility(false /* restoreImeVisibility */);
+
+        assertThat(
+                        startInputOrWindowGainedFocus(
+                                DEFAULT_SOFT_INPUT_FLAG, true /* forwardNavigation */))
+                .isEqualTo(SUCCESS_WAITING_IME_BINDING_RESULT);
+
+        switch (mSoftInputState) {
+            case SOFT_INPUT_STATE_UNSPECIFIED:
+                boolean showSoftInput = mSoftInputAdjustment == SOFT_INPUT_ADJUST_RESIZE;
+                verifyShowSoftInput(
+                        showSoftInput /* setVisible */, showSoftInput /* showSoftInput */);
+                // Soft input was hidden by default, so it doesn't need to call
+                // {@code IMS#hideSoftInput()}.
+                verifyHideSoftInput(!showSoftInput /* setNotVisible */, false /* hideSoftInput */);
+                break;
+            case SOFT_INPUT_STATE_VISIBLE:
+            case SOFT_INPUT_STATE_ALWAYS_VISIBLE:
+                verifyShowSoftInput(true /* setVisible */, true /* showSoftInput */);
+                verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */);
+                break;
+            case SOFT_INPUT_STATE_UNCHANGED: // Do nothing
+                verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
+                verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */);
+                break;
+            case SOFT_INPUT_STATE_HIDDEN:
+            case SOFT_INPUT_STATE_ALWAYS_HIDDEN:
+                verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
+                // Soft input was hidden by default, so it doesn't need to call
+                // {@code IMS#hideSoftInput()}.
+                verifyHideSoftInput(true /* setNotVisible */, false /* hideSoftInput */);
+                break;
+            default:
+                throw new IllegalStateException(
+                        "Unhandled soft input mode: "
+                                + InputMethodDebug.softInputModeToString(mSoftInputState));
+        }
+    }
+
+    @Test
+    public void startInputOrWindowGainedFocus_notForwardNavigation() throws RemoteException {
+        mockHasImeFocusAndRestoreImeVisibility(false /* restoreImeVisibility */);
+
+        assertThat(
+                        startInputOrWindowGainedFocus(
+                                DEFAULT_SOFT_INPUT_FLAG, false /* forwardNavigation */))
+                .isEqualTo(SUCCESS_WAITING_IME_BINDING_RESULT);
+
+        switch (mSoftInputState) {
+            case SOFT_INPUT_STATE_UNSPECIFIED:
+                boolean hideSoftInput = mSoftInputAdjustment != SOFT_INPUT_ADJUST_RESIZE;
+                verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
+                // Soft input was hidden by default, so it doesn't need to call
+                // {@code IMS#hideSoftInput()}.
+                verifyHideSoftInput(hideSoftInput /* setNotVisible */, false /* hideSoftInput */);
+                break;
+            case SOFT_INPUT_STATE_VISIBLE:
+            case SOFT_INPUT_STATE_HIDDEN:
+            case SOFT_INPUT_STATE_UNCHANGED: // Do nothing
+                verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
+                verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */);
+                break;
+            case SOFT_INPUT_STATE_ALWAYS_VISIBLE:
+                verifyShowSoftInput(true /* setVisible */, true /* showSoftInput */);
+                verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */);
+                break;
+            case SOFT_INPUT_STATE_ALWAYS_HIDDEN:
+                verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
+                // Soft input was hidden by default, so it doesn't need to call
+                // {@code IMS#hideSoftInput()}.
+                verifyHideSoftInput(true /* setNotVisible */, false /* hideSoftInput */);
+                break;
+            default:
+                throw new IllegalStateException(
+                        "Unhandled soft input mode: "
+                                + InputMethodDebug.softInputModeToString(mSoftInputState));
+        }
+    }
+
+    @Test
+    public void startInputOrWindowGainedFocus_userNotRunning() throws RemoteException {
+        when(mMockUserManagerInternal.isUserRunning(anyInt())).thenReturn(false);
+
+        assertThat(
+                        startInputOrWindowGainedFocus(
+                                DEFAULT_SOFT_INPUT_FLAG, true /* forwardNavigation */))
+                .isEqualTo(InputBindResult.INVALID_USER);
+        verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
+        verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */);
+    }
+
+    @Test
+    public void startInputOrWindowGainedFocus_invalidFocusStatus() throws RemoteException {
+        int[] invalidImeClientFocus =
+                new int[] {
+                    WindowManagerInternal.ImeClientFocusResult.NOT_IME_TARGET_WINDOW,
+                    WindowManagerInternal.ImeClientFocusResult.DISPLAY_ID_MISMATCH,
+                    WindowManagerInternal.ImeClientFocusResult.INVALID_DISPLAY_ID
+                };
+        InputBindResult[] inputBingResult =
+                new InputBindResult[] {
+                    InputBindResult.NOT_IME_TARGET_WINDOW,
+                    InputBindResult.DISPLAY_ID_MISMATCH,
+                    InputBindResult.INVALID_DISPLAY_ID
+                };
+
+        for (int i = 0; i < invalidImeClientFocus.length; i++) {
+            when(mMockWindowManagerInternal.hasInputMethodClientFocus(
+                            any(), anyInt(), anyInt(), anyInt()))
+                    .thenReturn(invalidImeClientFocus[i]);
+
+            assertThat(
+                            startInputOrWindowGainedFocus(
+                                    DEFAULT_SOFT_INPUT_FLAG, true /* forwardNavigation */))
+                    .isEqualTo(inputBingResult[i]);
+            verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
+            verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */);
+        }
+    }
+
+    private InputBindResult startInputOrWindowGainedFocus(
+            int startInputFlag, boolean forwardNavigation) {
+        int softInputMode = mSoftInputState | mSoftInputAdjustment;
+        if (forwardNavigation) {
+            softInputMode |= SOFT_INPUT_IS_FORWARD_NAVIGATION;
+        }
+
+        Log.i(
+                TAG,
+                "startInputOrWindowGainedFocus() softInputStateFlag="
+                        + InputMethodDebug.softInputModeToString(mSoftInputState)
+                        + ", softInputAdjustFlag="
+                        + InputMethodDebug.softInputModeToString(mSoftInputAdjustment));
+
+        return mInputMethodManagerService.startInputOrWindowGainedFocus(
+                StartInputReason.WINDOW_FOCUS_GAIN /* startInputReason */,
+                mMockInputMethodClient /* client */,
+                mWindowToken /* windowToken */,
+                startInputFlag /* startInputFlags */,
+                softInputMode /* softInputMode */,
+                0 /* windowFlags */,
+                mEditorInfo /* editorInfo */,
+                mMockRemoteInputConnection /* inputConnection */,
+                mMockRemoteAccessibilityInputConnection /* remoteAccessibilityInputConnection */,
+                mTargetSdkVersion /* unverifiedTargetSdkVersion */,
+                mCallingUserId /* userId */,
+                mMockImeOnBackInvokedDispatcher /* imeDispatcher */);
+    }
+
+    private void mockHasImeFocusAndRestoreImeVisibility(boolean restoreImeVisibility) {
+        when(mMockWindowManagerInternal.hasInputMethodClientFocus(
+                        any(), anyInt(), anyInt(), anyInt()))
+                .thenReturn(WindowManagerInternal.ImeClientFocusResult.HAS_IME_FOCUS);
+        when(mMockWindowManagerInternal.shouldRestoreImeVisibility(any()))
+                .thenReturn(restoreImeVisibility);
+    }
+}