1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.inputmethod;
18 
19 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING;
20 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN;
21 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
22 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED;
23 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
24 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN;
25 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE;
26 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN;
27 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED;
28 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED;
29 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE;
30 
31 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
32 
33 import static com.google.common.truth.Truth.assertThat;
34 
35 import static org.mockito.ArgumentMatchers.any;
36 import static org.mockito.ArgumentMatchers.anyInt;
37 import static org.mockito.Mockito.when;
38 
39 import android.os.IBinder;
40 import android.os.LocaleList;
41 import android.os.RemoteException;
42 import android.util.Log;
43 import android.view.inputmethod.EditorInfo;
44 import android.window.ImeOnBackInvokedDispatcher;
45 
46 import com.android.internal.inputmethod.IInputMethodClient;
47 import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection;
48 import com.android.internal.inputmethod.IRemoteInputConnection;
49 import com.android.internal.inputmethod.InputBindResult;
50 import com.android.internal.inputmethod.InputMethodDebug;
51 import com.android.internal.inputmethod.StartInputFlags;
52 import com.android.internal.inputmethod.StartInputReason;
53 import com.android.server.LocalServices;
54 import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
55 import com.android.server.wm.WindowManagerInternal;
56 
57 import org.junit.Test;
58 import org.junit.runner.RunWith;
59 import org.junit.runners.Parameterized;
60 import org.mockito.Mock;
61 
62 import java.util.ArrayList;
63 import java.util.List;
64 
65 /**
66  * Test the behavior of {@link InputMethodManagerService#startInputOrWindowGainedFocus(int,
67  * IInputMethodClient, IBinder, int, int, int, EditorInfo, IRemoteInputConnection,
68  * IRemoteAccessibilityInputConnection, int, int, ImeOnBackInvokedDispatcher)}.
69  */
70 @RunWith(Parameterized.class)
71 public class InputMethodManagerServiceWindowGainedFocusTest
72         extends InputMethodManagerServiceTestBase {
73     private static final String TAG = "IMMSWindowGainedFocusTest";
74 
75     private static final int[] SOFT_INPUT_STATE_FLAGS =
76             new int[] {
77                 SOFT_INPUT_STATE_UNSPECIFIED,
78                 SOFT_INPUT_STATE_UNCHANGED,
79                 SOFT_INPUT_STATE_HIDDEN,
80                 SOFT_INPUT_STATE_ALWAYS_HIDDEN,
81                 SOFT_INPUT_STATE_VISIBLE,
82                 SOFT_INPUT_STATE_ALWAYS_VISIBLE
83             };
84     private static final int[] SOFT_INPUT_ADJUST_FLAGS =
85             new int[] {
86                 SOFT_INPUT_ADJUST_UNSPECIFIED,
87                 SOFT_INPUT_ADJUST_RESIZE,
88                 SOFT_INPUT_ADJUST_PAN,
89                 SOFT_INPUT_ADJUST_NOTHING
90             };
91     private static final int DEFAULT_SOFT_INPUT_FLAG =
92             StartInputFlags.VIEW_HAS_FOCUS | StartInputFlags.IS_TEXT_EDITOR;
93     @Mock
94     VirtualDeviceManagerInternal mMockVdmInternal;
95 
96     @Parameterized.Parameters(name = "softInputState={0}, softInputAdjustment={1}")
softInputModeConfigs()97     public static List<Object[]> softInputModeConfigs() {
98         ArrayList<Object[]> params = new ArrayList<>();
99         for (int softInputState : SOFT_INPUT_STATE_FLAGS) {
100             for (int softInputAdjust : SOFT_INPUT_ADJUST_FLAGS) {
101                 params.add(new Object[] {softInputState, softInputAdjust});
102             }
103         }
104         return params;
105     }
106 
107     private final int mSoftInputState;
108     private final int mSoftInputAdjustment;
109 
InputMethodManagerServiceWindowGainedFocusTest( int softInputState, int softInputAdjustment)110     public InputMethodManagerServiceWindowGainedFocusTest(
111             int softInputState, int softInputAdjustment) {
112         mSoftInputState = softInputState;
113         mSoftInputAdjustment = softInputAdjustment;
114     }
115 
116     @Test
startInputOrWindowGainedFocus_forwardNavigation()117     public void startInputOrWindowGainedFocus_forwardNavigation() throws RemoteException {
118         mockHasImeFocusAndRestoreImeVisibility(false /* restoreImeVisibility */);
119 
120         assertThat(
121                         startInputOrWindowGainedFocus(
122                                 DEFAULT_SOFT_INPUT_FLAG, true /* forwardNavigation */))
123                 .isEqualTo(SUCCESS_WAITING_IME_BINDING_RESULT);
124 
125         switch (mSoftInputState) {
126             case SOFT_INPUT_STATE_UNSPECIFIED:
127                 boolean showSoftInput =
128                         (mSoftInputAdjustment == SOFT_INPUT_ADJUST_RESIZE) || mIsLargeScreen;
129                 verifyShowSoftInput(
130                         showSoftInput /* setVisible */, showSoftInput /* showSoftInput */);
131                 // Soft input was hidden by default, so it doesn't need to call
132                 // {@code IMS#hideSoftInput()}.
133                 verifyHideSoftInput(!showSoftInput /* setNotVisible */, false /* hideSoftInput */);
134                 break;
135             case SOFT_INPUT_STATE_VISIBLE:
136             case SOFT_INPUT_STATE_ALWAYS_VISIBLE:
137                 verifyShowSoftInput(true /* setVisible */, true /* showSoftInput */);
138                 verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */);
139                 break;
140             case SOFT_INPUT_STATE_UNCHANGED: // Do nothing
141                 verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
142                 verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */);
143                 break;
144             case SOFT_INPUT_STATE_HIDDEN:
145             case SOFT_INPUT_STATE_ALWAYS_HIDDEN:
146                 verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
147                 // Soft input was hidden by default, so it doesn't need to call
148                 // {@code IMS#hideSoftInput()}.
149                 verifyHideSoftInput(true /* setNotVisible */, false /* hideSoftInput */);
150                 break;
151             default:
152                 throw new IllegalStateException(
153                         "Unhandled soft input mode: "
154                                 + InputMethodDebug.softInputModeToString(mSoftInputState));
155         }
156     }
157 
158     @Test
startInputOrWindowGainedFocus_notForwardNavigation()159     public void startInputOrWindowGainedFocus_notForwardNavigation() throws RemoteException {
160         mockHasImeFocusAndRestoreImeVisibility(false /* restoreImeVisibility */);
161 
162         assertThat(
163                         startInputOrWindowGainedFocus(
164                                 DEFAULT_SOFT_INPUT_FLAG, false /* forwardNavigation */))
165                 .isEqualTo(SUCCESS_WAITING_IME_BINDING_RESULT);
166 
167         switch (mSoftInputState) {
168             case SOFT_INPUT_STATE_UNSPECIFIED:
169                 boolean hideSoftInput =
170                         (mSoftInputAdjustment != SOFT_INPUT_ADJUST_RESIZE) && !mIsLargeScreen;
171                 verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
172                 // Soft input was hidden by default, so it doesn't need to call
173                 // {@code IMS#hideSoftInput()}.
174                 verifyHideSoftInput(hideSoftInput /* setNotVisible */, false /* hideSoftInput */);
175                 break;
176             case SOFT_INPUT_STATE_VISIBLE:
177             case SOFT_INPUT_STATE_HIDDEN:
178             case SOFT_INPUT_STATE_UNCHANGED: // Do nothing
179                 verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
180                 verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */);
181                 break;
182             case SOFT_INPUT_STATE_ALWAYS_VISIBLE:
183                 verifyShowSoftInput(true /* setVisible */, true /* showSoftInput */);
184                 verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */);
185                 break;
186             case SOFT_INPUT_STATE_ALWAYS_HIDDEN:
187                 verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
188                 // Soft input was hidden by default, so it doesn't need to call
189                 // {@code IMS#hideSoftInput()}.
190                 verifyHideSoftInput(true /* setNotVisible */, false /* hideSoftInput */);
191                 break;
192             default:
193                 throw new IllegalStateException(
194                         "Unhandled soft input mode: "
195                                 + InputMethodDebug.softInputModeToString(mSoftInputState));
196         }
197     }
198 
199     @Test
startInputOrWindowGainedFocus_userNotRunning()200     public void startInputOrWindowGainedFocus_userNotRunning() throws RemoteException {
201         when(mMockUserManagerInternal.isUserRunning(anyInt())).thenReturn(false);
202 
203         assertThat(
204                         startInputOrWindowGainedFocus(
205                                 DEFAULT_SOFT_INPUT_FLAG, true /* forwardNavigation */))
206                 .isEqualTo(InputBindResult.INVALID_USER);
207         verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
208         verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */);
209     }
210 
211     @Test
startInputOrWindowGainedFocus_invalidFocusStatus()212     public void startInputOrWindowGainedFocus_invalidFocusStatus() throws RemoteException {
213         int[] invalidImeClientFocus =
214                 new int[] {
215                     WindowManagerInternal.ImeClientFocusResult.NOT_IME_TARGET_WINDOW,
216                     WindowManagerInternal.ImeClientFocusResult.DISPLAY_ID_MISMATCH,
217                     WindowManagerInternal.ImeClientFocusResult.INVALID_DISPLAY_ID
218                 };
219         InputBindResult[] inputBingResult =
220                 new InputBindResult[] {
221                     InputBindResult.NOT_IME_TARGET_WINDOW,
222                     InputBindResult.DISPLAY_ID_MISMATCH,
223                     InputBindResult.INVALID_DISPLAY_ID
224                 };
225 
226         for (int i = 0; i < invalidImeClientFocus.length; i++) {
227             when(mMockWindowManagerInternal.hasInputMethodClientFocus(
228                             any(), anyInt(), anyInt(), anyInt()))
229                     .thenReturn(invalidImeClientFocus[i]);
230 
231             assertThat(
232                             startInputOrWindowGainedFocus(
233                                     DEFAULT_SOFT_INPUT_FLAG, true /* forwardNavigation */))
234                     .isEqualTo(inputBingResult[i]);
235             verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
236             verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */);
237         }
238     }
239 
startInputOrWindowGainedFocus( int startInputFlag, boolean forwardNavigation)240     private InputBindResult startInputOrWindowGainedFocus(
241             int startInputFlag, boolean forwardNavigation) {
242         int softInputMode = mSoftInputState | mSoftInputAdjustment;
243         if (forwardNavigation) {
244             softInputMode |= SOFT_INPUT_IS_FORWARD_NAVIGATION;
245         }
246 
247         Log.i(
248                 TAG,
249                 "startInputOrWindowGainedFocus() softInputStateFlag="
250                         + InputMethodDebug.softInputModeToString(mSoftInputState)
251                         + ", softInputAdjustFlag="
252                         + InputMethodDebug.softInputModeToString(mSoftInputAdjustment));
253 
254         return mInputMethodManagerService.startInputOrWindowGainedFocus(
255                 StartInputReason.WINDOW_FOCUS_GAIN /* startInputReason */,
256                 mMockInputMethodClient /* client */,
257                 mWindowToken /* windowToken */,
258                 startInputFlag /* startInputFlags */,
259                 softInputMode /* softInputMode */,
260                 0 /* windowFlags */,
261                 mEditorInfo /* editorInfo */,
262                 mMockRemoteInputConnection /* inputConnection */,
263                 mMockRemoteAccessibilityInputConnection /* remoteAccessibilityInputConnection */,
264                 mTargetSdkVersion /* unverifiedTargetSdkVersion */,
265                 mCallingUserId /* userId */,
266                 mMockImeOnBackInvokedDispatcher /* imeDispatcher */);
267     }
268 
269     @Test
startInputOrWindowGainedFocus_localeHintsOverride()270     public void startInputOrWindowGainedFocus_localeHintsOverride() throws RemoteException {
271         doReturn(mMockVdmInternal).when(
272                 () -> LocalServices.getService(VirtualDeviceManagerInternal.class));
273         LocaleList overrideLocale = LocaleList.forLanguageTags("zh-CN");
274         doReturn(overrideLocale).when(mMockVdmInternal).getPreferredLocaleListForUid(anyInt());
275         mockHasImeFocusAndRestoreImeVisibility(false /* restoreImeVisibility */);
276 
277         assertThat(startInputOrWindowGainedFocus(DEFAULT_SOFT_INPUT_FLAG,
278                 true /* forwardNavigation */)).isEqualTo(SUCCESS_WAITING_IME_BINDING_RESULT);
279         assertThat(mEditorInfo.hintLocales).isEqualTo(overrideLocale);
280     }
281 
mockHasImeFocusAndRestoreImeVisibility(boolean restoreImeVisibility)282     private void mockHasImeFocusAndRestoreImeVisibility(boolean restoreImeVisibility) {
283         when(mMockWindowManagerInternal.hasInputMethodClientFocus(
284                         any(), anyInt(), anyInt(), anyInt()))
285                 .thenReturn(WindowManagerInternal.ImeClientFocusResult.HAS_IME_FOCUS);
286         when(mMockWindowManagerInternal.shouldRestoreImeVisibility(any()))
287                 .thenReturn(restoreImeVisibility);
288     }
289 }
290