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.accessibilityservice.AccessibilityService.SHOW_MODE_HIDDEN;
20 import static android.view.Display.DEFAULT_DISPLAY;
21 import static android.view.Display.INVALID_DISPLAY;
22 import static android.view.WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
23 import static android.view.WindowManager.DISPLAY_IME_POLICY_HIDE;
24 import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL;
25 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED;
26 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
27 
28 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
29 import static com.android.internal.inputmethod.SoftInputShowHideReason.HIDE_WHEN_INPUT_TARGET_INVISIBLE;
30 import static com.android.internal.inputmethod.SoftInputShowHideReason.REMOVE_IME_SCREENSHOT_FROM_IMMS;
31 import static com.android.internal.inputmethod.SoftInputShowHideReason.SHOW_IME_SCREENSHOT_FROM_IMMS;
32 import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeTargetWindowState;
33 import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeVisibilityResult;
34 import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME_EXPLICIT;
35 import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_REMOVE_IME_SNAPSHOT;
36 import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_SHOW_IME_SNAPSHOT;
37 import static com.android.server.inputmethod.InputMethodManagerService.FALLBACK_DISPLAY_ID;
38 import static com.android.server.inputmethod.InputMethodManagerService.ImeDisplayValidator;
39 
40 import static com.google.common.truth.Truth.assertThat;
41 
42 import android.os.Binder;
43 import android.os.IBinder;
44 import android.os.RemoteException;
45 import android.view.inputmethod.InputMethodManager;
46 
47 import androidx.test.ext.junit.runners.AndroidJUnit4;
48 
49 import com.android.server.wm.ImeTargetChangeListener;
50 import com.android.server.wm.WindowManagerInternal;
51 
52 import org.junit.Before;
53 import org.junit.Test;
54 import org.junit.runner.RunWith;
55 import org.mockito.ArgumentCaptor;
56 
57 /**
58  * Test the behavior of {@link ImeVisibilityStateComputer} and {@link ImeVisibilityApplier} when
59  * requesting the IME visibility.
60  *
61  * Build/Install/Run:
62  * atest FrameworksInputMethodSystemServerTests:ImeVisibilityStateComputerTest
63  */
64 @RunWith(AndroidJUnit4.class)
65 public class ImeVisibilityStateComputerTest extends InputMethodManagerServiceTestBase {
66     private ImeVisibilityStateComputer mComputer;
67     private ImeTargetChangeListener mListener;
68     private int mImeDisplayPolicy = DISPLAY_IME_POLICY_LOCAL;
69 
70     @Before
setUp()71     public void setUp() throws RemoteException {
72         super.setUp();
73         ImeVisibilityStateComputer.Injector injector = new ImeVisibilityStateComputer.Injector() {
74             @Override
75             public WindowManagerInternal getWmService() {
76                 return mMockWindowManagerInternal;
77             }
78 
79             @Override
80             public ImeDisplayValidator getImeValidator() {
81                 return displayId -> mImeDisplayPolicy;
82             }
83         };
84         ArgumentCaptor<ImeTargetChangeListener> captor = ArgumentCaptor.forClass(
85                 ImeTargetChangeListener.class);
86         verify(mMockWindowManagerInternal).setInputMethodTargetChangeListener(captor.capture());
87         mComputer = new ImeVisibilityStateComputer(mInputMethodManagerService, injector);
88         mListener = captor.getValue();
89     }
90 
91     @Test
testRequestImeVisibility_showImplicit()92     public void testRequestImeVisibility_showImplicit() {
93         initImeTargetWindowState(mWindowToken);
94         boolean res = mComputer.onImeShowFlags(null, InputMethodManager.SHOW_IMPLICIT);
95         mComputer.requestImeVisibility(mWindowToken, res);
96 
97         final ImeTargetWindowState state = mComputer.getWindowStateOrNull(mWindowToken);
98         assertThat(state).isNotNull();
99         assertThat(state.hasEditorFocused()).isTrue();
100         assertThat(state.getSoftInputModeState()).isEqualTo(SOFT_INPUT_STATE_UNCHANGED);
101         assertThat(state.isRequestedImeVisible()).isTrue();
102 
103         assertThat(mComputer.mRequestedShowExplicitly).isFalse();
104     }
105 
106     @Test
testRequestImeVisibility_showExplicit()107     public void testRequestImeVisibility_showExplicit() {
108         initImeTargetWindowState(mWindowToken);
109         boolean res = mComputer.onImeShowFlags(null, 0 /* showFlags */);
110         mComputer.requestImeVisibility(mWindowToken, res);
111 
112         final ImeTargetWindowState state = mComputer.getWindowStateOrNull(mWindowToken);
113         assertThat(state).isNotNull();
114         assertThat(state.hasEditorFocused()).isTrue();
115         assertThat(state.getSoftInputModeState()).isEqualTo(SOFT_INPUT_STATE_UNCHANGED);
116         assertThat(state.isRequestedImeVisible()).isTrue();
117 
118         assertThat(mComputer.mRequestedShowExplicitly).isTrue();
119     }
120 
121     /**
122      * This checks that the state after an explicit show request does not get reset during
123      * a subsequent implicit show request, without an intermediary hide request.
124      */
125     @Test
testRequestImeVisibility_showExplicit_thenShowImplicit()126     public void testRequestImeVisibility_showExplicit_thenShowImplicit() {
127         initImeTargetWindowState(mWindowToken);
128         mComputer.onImeShowFlags(null, 0 /* showFlags */);
129         assertThat(mComputer.mRequestedShowExplicitly).isTrue();
130 
131         mComputer.onImeShowFlags(null, InputMethodManager.SHOW_IMPLICIT);
132         assertThat(mComputer.mRequestedShowExplicitly).isTrue();
133     }
134 
135     /**
136      * This checks that the state after a forced show request does not get reset during
137      * a subsequent explicit show request, without an intermediary hide request.
138      */
139     @Test
testRequestImeVisibility_showForced_thenShowExplicit()140     public void testRequestImeVisibility_showForced_thenShowExplicit() {
141         initImeTargetWindowState(mWindowToken);
142         mComputer.onImeShowFlags(null, InputMethodManager.SHOW_FORCED);
143         assertThat(mComputer.mShowForced).isTrue();
144 
145         mComputer.onImeShowFlags(null, 0 /* showFlags */);
146         assertThat(mComputer.mShowForced).isTrue();
147     }
148 
149     @Test
testRequestImeVisibility_showImplicit_a11yNoImePolicy()150     public void testRequestImeVisibility_showImplicit_a11yNoImePolicy() {
151         // Precondition: set AccessibilityService#SHOW_MODE_HIDDEN policy
152         mComputer.getImePolicy().setA11yRequestNoSoftKeyboard(SHOW_MODE_HIDDEN);
153 
154         initImeTargetWindowState(mWindowToken);
155         boolean res = mComputer.onImeShowFlags(null, InputMethodManager.SHOW_IMPLICIT);
156         mComputer.requestImeVisibility(mWindowToken, res);
157 
158         final ImeTargetWindowState state = mComputer.getWindowStateOrNull(mWindowToken);
159         assertThat(state).isNotNull();
160         assertThat(state.hasEditorFocused()).isTrue();
161         assertThat(state.getSoftInputModeState()).isEqualTo(SOFT_INPUT_STATE_UNCHANGED);
162         assertThat(state.isRequestedImeVisible()).isFalse();
163 
164         assertThat(mComputer.mRequestedShowExplicitly).isFalse();
165     }
166 
167     @Test
testRequestImeVisibility_showImplicit_imeHiddenPolicy()168     public void testRequestImeVisibility_showImplicit_imeHiddenPolicy() {
169         // Precondition: set IME hidden display policy before calling showSoftInput
170         mComputer.getImePolicy().setImeHiddenByDisplayPolicy(true);
171 
172         initImeTargetWindowState(mWindowToken);
173         boolean res = mComputer.onImeShowFlags(null, InputMethodManager.SHOW_IMPLICIT);
174         mComputer.requestImeVisibility(mWindowToken, res);
175 
176         final ImeTargetWindowState state = mComputer.getWindowStateOrNull(mWindowToken);
177         assertThat(state).isNotNull();
178         assertThat(state.hasEditorFocused()).isTrue();
179         assertThat(state.getSoftInputModeState()).isEqualTo(SOFT_INPUT_STATE_UNCHANGED);
180         assertThat(state.isRequestedImeVisible()).isFalse();
181 
182         assertThat(mComputer.mRequestedShowExplicitly).isFalse();
183     }
184 
185     @Test
testRequestImeVisibility_hideNotAlways()186     public void testRequestImeVisibility_hideNotAlways() {
187         // Precondition: ensure IME has shown before hiding request.
188         mComputer.setInputShown(true);
189 
190         initImeTargetWindowState(mWindowToken);
191         assertThat(mComputer.canHideIme(null, InputMethodManager.HIDE_NOT_ALWAYS)).isTrue();
192         mComputer.requestImeVisibility(mWindowToken, false);
193 
194         final ImeTargetWindowState state = mComputer.getWindowStateOrNull(mWindowToken);
195         assertThat(state).isNotNull();
196         assertThat(state.hasEditorFocused()).isTrue();
197         assertThat(state.getSoftInputModeState()).isEqualTo(SOFT_INPUT_STATE_UNCHANGED);
198         assertThat(state.isRequestedImeVisible()).isFalse();
199     }
200 
201     @Test
testComputeImeDisplayId()202     public void testComputeImeDisplayId() {
203         final ImeTargetWindowState state = mComputer.getOrCreateWindowState(mWindowToken);
204 
205         mImeDisplayPolicy = DISPLAY_IME_POLICY_LOCAL;
206         mComputer.computeImeDisplayId(state, DEFAULT_DISPLAY);
207         assertThat(mComputer.getImePolicy().isImeHiddenByDisplayPolicy()).isFalse();
208         assertThat(state.getImeDisplayId()).isEqualTo(DEFAULT_DISPLAY);
209 
210         mComputer.computeImeDisplayId(state, 10 /* displayId */);
211         assertThat(mComputer.getImePolicy().isImeHiddenByDisplayPolicy()).isFalse();
212         assertThat(state.getImeDisplayId()).isEqualTo(10);
213 
214         mImeDisplayPolicy = DISPLAY_IME_POLICY_HIDE;
215         mComputer.computeImeDisplayId(state, 10 /* displayId */);
216         assertThat(mComputer.getImePolicy().isImeHiddenByDisplayPolicy()).isTrue();
217         assertThat(state.getImeDisplayId()).isEqualTo(INVALID_DISPLAY);
218 
219         mImeDisplayPolicy = DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
220         mComputer.computeImeDisplayId(state, 10 /* displayId */);
221         assertThat(mComputer.getImePolicy().isImeHiddenByDisplayPolicy()).isFalse();
222         assertThat(state.getImeDisplayId()).isEqualTo(FALLBACK_DISPLAY_ID);
223     }
224 
225     @Test
testComputeState_lastImeRequestedVisible_preserved_When_StateUnChanged()226     public void testComputeState_lastImeRequestedVisible_preserved_When_StateUnChanged() {
227         // Assume the last IME targeted window has requested IME visible
228         final IBinder lastImeTargetWindowToken = new Binder();
229         mInputMethodManagerService.mLastImeTargetWindow = lastImeTargetWindowToken;
230         mComputer.requestImeVisibility(lastImeTargetWindowToken, true);
231         final ImeTargetWindowState lastState = mComputer.getWindowStateOrNull(
232                 lastImeTargetWindowToken);
233         assertThat(lastState.isRequestedImeVisible()).isTrue();
234 
235         // Verify when focusing the next window with STATE_UNCHANGED flag, the last IME
236         // visibility state will be preserved to the current window state.
237         final ImeTargetWindowState stateWithUnChangedFlag = initImeTargetWindowState(mWindowToken);
238         mComputer.computeState(stateWithUnChangedFlag, true /* allowVisible */);
239         assertThat(stateWithUnChangedFlag.isRequestedImeVisible()).isEqualTo(
240                 lastState.isRequestedImeVisible());
241     }
242 
243     @Test
testOnInteractiveChanged()244     public void testOnInteractiveChanged() {
245         mComputer.getOrCreateWindowState(mWindowToken);
246         // Precondition: ensure IME has shown before hiding request.
247         mComputer.requestImeVisibility(mWindowToken, true);
248         mComputer.setInputShown(true);
249 
250         // No need any visibility change When initially shows IME on the device was interactive.
251         ImeVisibilityStateComputer.ImeVisibilityResult result = mComputer.onInteractiveChanged(
252                 mWindowToken, true /* interactive */);
253         assertThat(result).isNull();
254 
255         // Show the IME screenshot to capture the last IME visible state when the device inactive.
256         result = mComputer.onInteractiveChanged(mWindowToken, false /* interactive */);
257         assertThat(result).isNotNull();
258         assertThat(result.getState()).isEqualTo(STATE_SHOW_IME_SNAPSHOT);
259         assertThat(result.getReason()).isEqualTo(SHOW_IME_SCREENSHOT_FROM_IMMS);
260 
261         // Remove the IME screenshot when the device became interactive again.
262         result = mComputer.onInteractiveChanged(mWindowToken, true /* interactive */);
263         assertThat(result).isNotNull();
264         assertThat(result.getState()).isEqualTo(STATE_REMOVE_IME_SNAPSHOT);
265         assertThat(result.getReason()).isEqualTo(REMOVE_IME_SCREENSHOT_FROM_IMMS);
266     }
267 
268     @Test
testOnApplyImeVisibilityFromComputer()269     public void testOnApplyImeVisibilityFromComputer() {
270         final IBinder testImeTargetOverlay = new Binder();
271         final IBinder testImeInputTarget = new Binder();
272 
273         // Simulate a test IME input target was visible.
274         mListener.onImeInputTargetVisibilityChanged(testImeInputTarget, true, false);
275 
276         // Simulate a test IME layering target overlay fully occluded the IME input target.
277         mListener.onImeTargetOverlayVisibilityChanged(testImeTargetOverlay,
278                 TYPE_APPLICATION_OVERLAY, true, false);
279         mListener.onImeInputTargetVisibilityChanged(testImeInputTarget, false, false);
280         final ArgumentCaptor<IBinder> targetCaptor = ArgumentCaptor.forClass(IBinder.class);
281         final ArgumentCaptor<ImeVisibilityResult> resultCaptor = ArgumentCaptor.forClass(
282                 ImeVisibilityResult.class);
283         verify(mInputMethodManagerService).onApplyImeVisibilityFromComputer(targetCaptor.capture(),
284                 resultCaptor.capture());
285         final IBinder imeInputTarget = targetCaptor.getValue();
286         final ImeVisibilityResult result = resultCaptor.getValue();
287 
288         // Verify the computer will callback hiding IME state to IMMS.
289         assertThat(imeInputTarget).isEqualTo(testImeInputTarget);
290         assertThat(result.getState()).isEqualTo(STATE_HIDE_IME_EXPLICIT);
291         assertThat(result.getReason()).isEqualTo(HIDE_WHEN_INPUT_TARGET_INVISIBLE);
292     }
293 
initImeTargetWindowState(IBinder windowToken)294     private ImeTargetWindowState initImeTargetWindowState(IBinder windowToken) {
295         final ImeTargetWindowState state = new ImeTargetWindowState(SOFT_INPUT_STATE_UNCHANGED,
296                 0, true, true, true);
297         mComputer.setWindowState(windowToken, state);
298         return state;
299     }
300 }
301