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 com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
20 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
21 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
22 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
23 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
24 
25 import static org.mockito.ArgumentMatchers.any;
26 import static org.mockito.ArgumentMatchers.anyBoolean;
27 import static org.mockito.ArgumentMatchers.anyInt;
28 import static org.mockito.ArgumentMatchers.anyLong;
29 import static org.mockito.ArgumentMatchers.anyString;
30 import static org.mockito.ArgumentMatchers.notNull;
31 import static org.mockito.Mockito.eq;
32 import static org.mockito.Mockito.mock;
33 import static org.mockito.Mockito.times;
34 import static org.mockito.Mockito.verify;
35 import static org.mockito.Mockito.when;
36 
37 import android.app.ActivityManagerInternal;
38 import android.content.Context;
39 import android.content.pm.PackageManagerInternal;
40 import android.content.res.Configuration;
41 import android.hardware.input.IInputManager;
42 import android.hardware.input.InputManagerGlobal;
43 import android.os.Binder;
44 import android.os.IBinder;
45 import android.os.Process;
46 import android.os.RemoteException;
47 import android.os.ServiceManager;
48 import android.os.UserHandle;
49 import android.view.InputChannel;
50 import android.view.inputmethod.EditorInfo;
51 import android.window.ImeOnBackInvokedDispatcher;
52 
53 import androidx.test.platform.app.InstrumentationRegistry;
54 
55 import com.android.internal.compat.IPlatformCompat;
56 import com.android.internal.inputmethod.IInputMethod;
57 import com.android.internal.inputmethod.IInputMethodClient;
58 import com.android.internal.inputmethod.IInputMethodSession;
59 import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection;
60 import com.android.internal.inputmethod.IRemoteInputConnection;
61 import com.android.internal.inputmethod.InputBindResult;
62 import com.android.internal.view.IInputMethodManager;
63 import com.android.server.LocalServices;
64 import com.android.server.ServiceThread;
65 import com.android.server.SystemServerInitThreadPool;
66 import com.android.server.SystemService;
67 import com.android.server.input.InputManagerInternal;
68 import com.android.server.pm.UserManagerInternal;
69 import com.android.server.wm.ImeTargetVisibilityPolicy;
70 import com.android.server.wm.WindowManagerInternal;
71 
72 import org.junit.After;
73 import org.junit.Before;
74 import org.junit.BeforeClass;
75 import org.mockito.Mock;
76 import org.mockito.MockitoSession;
77 import org.mockito.quality.Strictness;
78 
79 /** Base class for testing {@link InputMethodManagerService}. */
80 public class InputMethodManagerServiceTestBase {
81     private static final int NO_VERIFY_SHOW_FLAGS = -1;
82 
83     protected static final String TEST_SELECTED_IME_ID = "test.ime";
84     protected static final String TEST_EDITOR_PKG_NAME = "test.editor";
85     protected static final String TEST_FOCUSED_WINDOW_NAME = "test.editor/activity";
86     protected static final WindowManagerInternal.ImeTargetInfo TEST_IME_TARGET_INFO =
87             new WindowManagerInternal.ImeTargetInfo(
88                     TEST_FOCUSED_WINDOW_NAME,
89                     TEST_FOCUSED_WINDOW_NAME,
90                     TEST_FOCUSED_WINDOW_NAME,
91                     TEST_FOCUSED_WINDOW_NAME,
92                     TEST_FOCUSED_WINDOW_NAME);
93     protected static final InputBindResult SUCCESS_WAITING_IME_BINDING_RESULT =
94             new InputBindResult(
95                     InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING,
96                     null,
97                     null,
98                     null,
99                     "0",
100                     0,
101                     false);
102 
103     @Mock protected WindowManagerInternal mMockWindowManagerInternal;
104     @Mock protected ActivityManagerInternal mMockActivityManagerInternal;
105     @Mock protected PackageManagerInternal mMockPackageManagerInternal;
106     @Mock protected InputManagerInternal mMockInputManagerInternal;
107     @Mock protected UserManagerInternal mMockUserManagerInternal;
108     @Mock protected InputMethodBindingController mMockInputMethodBindingController;
109     @Mock protected IInputMethodClient mMockInputMethodClient;
110     @Mock protected IInputMethodSession mMockInputMethodSession;
111     @Mock protected IBinder mWindowToken;
112     @Mock protected IRemoteInputConnection mMockRemoteInputConnection;
113     @Mock protected IRemoteAccessibilityInputConnection mMockRemoteAccessibilityInputConnection;
114     @Mock protected ImeOnBackInvokedDispatcher mMockImeOnBackInvokedDispatcher;
115     @Mock protected IInputMethodManager.Stub mMockIInputMethodManager;
116     @Mock protected IPlatformCompat.Stub mMockIPlatformCompat;
117     @Mock protected IInputMethod mMockInputMethod;
118     @Mock protected IBinder mMockInputMethodBinder;
119     @Mock protected IInputManager mMockIInputManager;
120     @Mock protected ImeTargetVisibilityPolicy mMockImeTargetVisibilityPolicy;
121 
122     protected Context mContext;
123     protected MockitoSession mMockingSession;
124     protected int mTargetSdkVersion;
125     protected int mCallingUserId;
126     protected EditorInfo mEditorInfo;
127     protected IInputMethodInvoker mMockInputMethodInvoker;
128     protected InputMethodManagerService mInputMethodManagerService;
129     protected ServiceThread mServiceThread;
130     protected ServiceThread mIoThread;
131     protected boolean mIsLargeScreen;
132     private InputManagerGlobal.TestSession mInputManagerGlobalSession;
133 
134     @BeforeClass
setupClass()135     public static void setupClass() {
136         // Make sure DeviceConfig's lazy-initialized ContentProvider gets
137         // a real instance before we stub out all system services below.
138         // TODO(b/272229177): remove dependency on real ContentProvider
139         new InputMethodDeviceConfigs().destroy();
140     }
141 
142     @Before
setUp()143     public void setUp() throws RemoteException {
144         mMockingSession =
145                 mockitoSession()
146                         .initMocks(this)
147                         .strictness(Strictness.LENIENT)
148                         .spyStatic(LocalServices.class)
149                         .mockStatic(ServiceManager.class)
150                         .mockStatic(SystemServerInitThreadPool.class)
151                         .startMocking();
152 
153         mContext = spy(InstrumentationRegistry.getInstrumentation().getContext());
154 
155         mTargetSdkVersion = mContext.getApplicationInfo().targetSdkVersion;
156         mIsLargeScreen = mContext.getResources().getConfiguration()
157                 .isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE);
158         mCallingUserId = UserHandle.getCallingUserId();
159         mEditorInfo = new EditorInfo();
160         mEditorInfo.packageName = TEST_EDITOR_PKG_NAME;
161 
162         // Injecting and mocking local services.
163         doReturn(mMockWindowManagerInternal)
164                 .when(() -> LocalServices.getService(WindowManagerInternal.class));
165         doReturn(mMockActivityManagerInternal)
166                 .when(() -> LocalServices.getService(ActivityManagerInternal.class));
167         doReturn(mMockPackageManagerInternal)
168                 .when(() -> LocalServices.getService(PackageManagerInternal.class));
169         doReturn(mMockInputManagerInternal)
170                 .when(() -> LocalServices.getService(InputManagerInternal.class));
171         doReturn(mMockUserManagerInternal)
172                 .when(() -> LocalServices.getService(UserManagerInternal.class));
173         doReturn(mMockImeTargetVisibilityPolicy)
174                 .when(() -> LocalServices.getService(ImeTargetVisibilityPolicy.class));
175         doReturn(mMockIInputMethodManager)
176                 .when(() -> ServiceManager.getServiceOrThrow(Context.INPUT_METHOD_SERVICE));
177         doReturn(mMockIPlatformCompat)
178                 .when(() -> ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
179 
180         // Stubbing out context related methods to avoid the system holding strong references to
181         // InputMethodManagerService.
182         doNothing().when(mContext).enforceCallingPermission(anyString(), anyString());
183         doNothing().when(mContext).sendBroadcastAsUser(any(), any());
184         doReturn(null).when(mContext).registerReceiver(any(), any());
185         doReturn(null)
186                 .when(mContext)
187                 .registerReceiverAsUser(any(), any(), any(), anyString(), any(), anyInt());
188 
189         // Injecting and mocked InputMethodBindingController and InputMethod.
190         mMockInputMethodInvoker = IInputMethodInvoker.create(mMockInputMethod);
191         mInputManagerGlobalSession = InputManagerGlobal.createTestSession(mMockIInputManager);
192         synchronized (ImfLock.class) {
193             when(mMockInputMethodBindingController.getCurMethod())
194                     .thenReturn(mMockInputMethodInvoker);
195             when(mMockInputMethodBindingController.bindCurrentMethod())
196                     .thenReturn(SUCCESS_WAITING_IME_BINDING_RESULT);
197             doNothing().when(mMockInputMethodBindingController).unbindCurrentMethod();
198             when(mMockInputMethodBindingController.getSelectedMethodId())
199                     .thenReturn(TEST_SELECTED_IME_ID);
200         }
201 
202         // Shuffling around all other initialization to make the test runnable.
203         when(mMockIInputManager.getInputDeviceIds()).thenReturn(new int[0]);
204         when(mMockIInputMethodManager.isImeTraceEnabled()).thenReturn(false);
205         when(mMockIPlatformCompat.isChangeEnabledByUid(anyLong(), anyInt())).thenReturn(true);
206         when(mMockUserManagerInternal.isUserRunning(anyInt())).thenReturn(true);
207         when(mMockUserManagerInternal.getProfileIds(anyInt(), anyBoolean()))
208                 .thenReturn(new int[] {0});
209         when(mMockUserManagerInternal.getUserIds()).thenReturn(new int[] {0});
210         when(mMockActivityManagerInternal.isSystemReady()).thenReturn(true);
211         when(mMockActivityManagerInternal.getCurrentUserId()).thenReturn(mCallingUserId);
212         when(mMockPackageManagerInternal.getPackageUid(anyString(), anyLong(), anyInt()))
213                 .thenReturn(Binder.getCallingUid());
214         when(mMockPackageManagerInternal.isSameApp(anyString(), anyLong(), anyInt(), anyInt()))
215             .thenReturn(true);
216         when(mMockWindowManagerInternal.onToggleImeRequested(anyBoolean(), any(), any(), anyInt()))
217                 .thenReturn(TEST_IME_TARGET_INFO);
218         when(mMockInputMethodClient.asBinder()).thenReturn(mMockInputMethodBinder);
219 
220         // Used by lazy initializing draw IMS nav bar at InputMethodManagerService#systemRunning(),
221         // which is ok to be mocked out for now.
222         doReturn(null).when(() -> SystemServerInitThreadPool.submit(any(), anyString()));
223 
224         mServiceThread =
225                 new ServiceThread(
226                         "immstest1",
227                         Process.THREAD_PRIORITY_FOREGROUND,
228                         true /* allowIo */);
229         mIoThread =
230                 new ServiceThread(
231                         "immstest2",
232                         Process.THREAD_PRIORITY_FOREGROUND,
233                         true /* allowIo */);
234         mInputMethodManagerService = new InputMethodManagerService(mContext,
235                 InputMethodManagerService.shouldEnableExperimentalConcurrentMultiUserMode(mContext),
236                 mServiceThread, mIoThread,
237                 unusedUserId -> mMockInputMethodBindingController);
238         spyOn(mInputMethodManagerService);
239 
240         // Start a InputMethodManagerService.Lifecycle to publish and manage the lifecycle of
241         // InputMethodManagerService, which is closer to the real situation.
242         InputMethodManagerService.Lifecycle lifecycle =
243                 new InputMethodManagerService.Lifecycle(mContext, mInputMethodManagerService);
244 
245         // Public local InputMethodManagerService.
246         LocalServices.removeServiceForTest(InputMethodManagerInternal.class);
247         lifecycle.onStart();
248         try {
249             // After this boot phase, services can broadcast Intents.
250             lifecycle.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY);
251         } catch (SecurityException e) {
252             // Security exception to permission denial is expected in test, mocking out to ensure
253             // InputMethodManagerService as system ready state.
254             if (!e.getMessage().contains("Permission Denial: not allowed to send broadcast")) {
255                 throw e;
256             }
257         }
258 
259         // Call InputMethodManagerService#addClient() as a preparation to start interacting with it.
260         mInputMethodManagerService.addClient(mMockInputMethodClient, mMockRemoteInputConnection, 0);
261         createSessionForClient(mMockInputMethodClient);
262     }
263 
264     @After
tearDown()265     public void tearDown() {
266         if (mInputMethodManagerService != null) {
267             mInputMethodManagerService.mInputMethodDeviceConfigs.destroy();
268         }
269 
270         if (mIoThread != null) {
271             mIoThread.quitSafely();
272         }
273 
274         if (mServiceThread != null) {
275             mServiceThread.quitSafely();
276         }
277 
278         if (mMockingSession != null) {
279             mMockingSession.finishMocking();
280         }
281 
282         if (mInputManagerGlobalSession != null) {
283             mInputManagerGlobalSession.close();
284         }
285         LocalServices.removeServiceForTest(InputMethodManagerInternal.class);
286     }
287 
verifyShowSoftInput(boolean setVisible, boolean showSoftInput)288     protected void verifyShowSoftInput(boolean setVisible, boolean showSoftInput)
289             throws RemoteException {
290         verifyShowSoftInput(setVisible, showSoftInput, NO_VERIFY_SHOW_FLAGS);
291     }
292 
verifyShowSoftInput(boolean setVisible, boolean showSoftInput, int showFlags)293     protected void verifyShowSoftInput(boolean setVisible, boolean showSoftInput, int showFlags)
294             throws RemoteException {
295         synchronized (ImfLock.class) {
296             verify(mMockInputMethodBindingController, times(setVisible ? 1 : 0))
297                     .setCurrentMethodVisible();
298         }
299         verify(mMockInputMethod, times(showSoftInput ? 1 : 0))
300                 .showSoftInput(any() /* showInputToken */ , notNull() /* statsToken */,
301                         showFlags != NO_VERIFY_SHOW_FLAGS ? eq(showFlags) : anyInt() /* flags*/,
302                         any() /* resultReceiver */);
303     }
304 
verifyHideSoftInput(boolean setNotVisible, boolean hideSoftInput)305     protected void verifyHideSoftInput(boolean setNotVisible, boolean hideSoftInput)
306             throws RemoteException {
307         synchronized (ImfLock.class) {
308             verify(mMockInputMethodBindingController, times(setNotVisible ? 1 : 0))
309                     .setCurrentMethodNotVisible();
310         }
311         verify(mMockInputMethod, times(hideSoftInput ? 1 : 0))
312                 .hideSoftInput(any() /* hideInputToken */, notNull() /* statsToken */,
313                         anyInt() /* flags */, any() /* resultReceiver */);
314     }
315 
createSessionForClient(IInputMethodClient client)316     protected void createSessionForClient(IInputMethodClient client) {
317         synchronized (ImfLock.class) {
318             ClientState cs = mInputMethodManagerService.getClientStateLocked(client);
319             cs.mCurSession = new InputMethodManagerService.SessionState(cs,
320                     mMockInputMethodInvoker, mMockInputMethodSession, mock(
321                     InputChannel.class));
322         }
323     }
324 }
325