diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 76495b17c984d..12c5aa8a2b1f8 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.view.inputmethod.InputMethodSubtype; 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 final class InputMethodManagerService extends IInputMethodManager.Stub } 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // 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 0000000000000..939fb6a9d1700 --- /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 0000000000000..12e7cfc28a56b --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/services/tests/InputMethodSystemServerTests/AndroidTest.xml b/services/tests/InputMethodSystemServerTests/AndroidTest.xml new file mode 100644 index 0000000000000..92be78060da80 --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/AndroidTest.xml @@ -0,0 +1,39 @@ + + + + diff --git a/services/tests/InputMethodSystemServerTests/OWNERS b/services/tests/InputMethodSystemServerTests/OWNERS new file mode 100644 index 0000000000000..1f2c036773a4f --- /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 0000000000000..3fbc4004785d2 --- /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 0000000000000..ffa2729434553 --- /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 softInputModeConfigs() { + ArrayList 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); + } +}