/* * 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.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; 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.LocaleList; 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.LocalServices; import com.android.server.companion.virtual.VirtualDeviceManagerInternal; import com.android.server.wm.WindowManagerInternal; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.mockito.Mock; 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; @Mock VirtualDeviceManagerInternal mMockVdmInternal; @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) || mIsLargeScreen; 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) && !mIsLargeScreen; 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 */); } @Test public void startInputOrWindowGainedFocus_localeHintsOverride() throws RemoteException { doReturn(mMockVdmInternal).when( () -> LocalServices.getService(VirtualDeviceManagerInternal.class)); LocaleList overrideLocale = LocaleList.forLanguageTags("zh-CN"); doReturn(overrideLocale).when(mMockVdmInternal).getPreferredLocaleListForUid(anyInt()); mockHasImeFocusAndRestoreImeVisibility(false /* restoreImeVisibility */); assertThat(startInputOrWindowGainedFocus(DEFAULT_SOFT_INPUT_FLAG, true /* forwardNavigation */)).isEqualTo(SUCCESS_WAITING_IME_BINDING_RESULT); assertThat(mEditorInfo.hintLocales).isEqualTo(overrideLocale); } private void mockHasImeFocusAndRestoreImeVisibility(boolean restoreImeVisibility) { when(mMockWindowManagerInternal.hasInputMethodClientFocus( any(), anyInt(), anyInt(), anyInt())) .thenReturn(WindowManagerInternal.ImeClientFocusResult.HAS_IME_FOCUS); when(mMockWindowManagerInternal.shouldRestoreImeVisibility(any())) .thenReturn(restoreImeVisibility); } }