1 /*
2  * Copyright (C) 2008 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 android.view.inputmethod.cts;
18 
19 import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS;
20 import static android.content.Intent.FLAG_RECEIVER_FOREGROUND;
21 import static android.content.pm.PackageManager.FEATURE_INPUT_METHODS;
22 import static android.view.inputmethod.cts.util.TestUtils.getOnMainSync;
23 import static android.view.inputmethod.cts.util.TestUtils.isInputMethodPickerShown;
24 import static android.view.inputmethod.cts.util.TestUtils.waitOnMainUntil;
25 
26 import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
27 
28 import static com.google.common.truth.Truth.assertThat;
29 
30 import static org.junit.Assert.assertFalse;
31 import static org.junit.Assert.assertNotNull;
32 import static org.junit.Assert.assertThrows;
33 import static org.junit.Assert.assertTrue;
34 import static org.junit.Assert.fail;
35 import static org.junit.Assume.assumeTrue;
36 
37 import android.app.Instrumentation;
38 import android.content.Context;
39 import android.content.Intent;
40 import android.content.pm.PackageManager;
41 import android.os.Debug;
42 import android.platform.test.annotations.AppModeFull;
43 import android.platform.test.annotations.AppModeSdkSandbox;
44 import android.platform.test.annotations.SecurityTest;
45 import android.text.TextUtils;
46 import android.view.View;
47 import android.view.inputmethod.EditorInfo;
48 import android.view.inputmethod.InputConnection;
49 import android.view.inputmethod.InputMethodInfo;
50 import android.view.inputmethod.InputMethodManager;
51 import android.view.inputmethod.InputMethodSubtype;
52 import android.view.inputmethod.cts.util.TestActivity;
53 import android.widget.EditText;
54 import android.widget.LinearLayout;
55 import android.widget.LinearLayout.LayoutParams;
56 
57 import androidx.annotation.NonNull;
58 import androidx.test.filters.MediumTest;
59 import androidx.test.platform.app.InstrumentationRegistry;
60 import androidx.test.runner.AndroidJUnit4;
61 import androidx.test.uiautomator.By;
62 import androidx.test.uiautomator.UiDevice;
63 import androidx.test.uiautomator.Until;
64 
65 import com.android.compatibility.common.util.PollingCheck;
66 
67 import org.junit.After;
68 import org.junit.Before;
69 import org.junit.Test;
70 import org.junit.runner.RunWith;
71 
72 import java.io.File;
73 import java.io.IOException;
74 import java.lang.ref.Cleaner;
75 import java.lang.reflect.Field;
76 import java.util.List;
77 import java.util.concurrent.CountDownLatch;
78 import java.util.concurrent.TimeUnit;
79 import java.util.concurrent.atomic.AtomicReference;
80 import java.util.stream.Collectors;
81 
82 @MediumTest
83 @RunWith(AndroidJUnit4.class)
84 @AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).")
85 public class InputMethodManagerTest {
86     private static final String MOCK_IME_ID = "com.android.cts.mockime/.MockIme";
87     private static final String MOCK_IME_LABEL = "Mock IME";
88     private static final String HIDDEN_FROM_PICKER_IME_ID =
89             "com.android.cts.hiddenfrompickerime/.HiddenFromPickerIme";
90     private static final String HIDDEN_FROM_PICKER_IME_LABEL = "Hidden From Picker IME";
91     private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5);
92     private Instrumentation mInstrumentation;
93     private Context mContext;
94     private InputMethodManager mImManager;
95     private boolean mNeedsImeReset = false;
96 
97     @Before
setup()98     public void setup() {
99         mInstrumentation = InstrumentationRegistry.getInstrumentation();
100         mContext = mInstrumentation.getTargetContext();
101         mImManager = mContext.getSystemService(InputMethodManager.class);
102     }
103 
104     @After
resetImes()105     public void resetImes() {
106         if (mNeedsImeReset) {
107             runShellCommandOrThrow("ime reset");
108             mNeedsImeReset = false;
109         }
110     }
111 
112     /**
113      * Verifies that the test API {@link InputMethodManager#isInputMethodPickerShown()} is properly
114      * protected with some permission.
115      *
116      * <p>This is a regression test for Bug 237317525.</p>
117      */
118     @SecurityTest(minPatchLevel = "unknown")
119     @Test
testIsInputMethodPickerShownProtection()120     public void testIsInputMethodPickerShownProtection() {
121         assumeTrue(mContext.getPackageManager().hasSystemFeature(FEATURE_INPUT_METHODS));
122         assertThrows("InputMethodManager#isInputMethodPickerShown() must not be accessible to "
123                 + "normal apps.", SecurityException.class, mImManager::isInputMethodPickerShown);
124     }
125 
126     /**
127      * Verifies that the test API {@link InputMethodManager#addVirtualStylusIdForTestSession()} is
128      * properly protected with some permission.
129      */
130     @Test
testAddVirtualStylusIdForTestSessionProtection()131     public void testAddVirtualStylusIdForTestSessionProtection() {
132         assumeTrue(mContext.getPackageManager().hasSystemFeature(FEATURE_INPUT_METHODS));
133         assertThrows("InputMethodManager#addVirtualStylusIdForTestSession() must not be accessible "
134                 + "to normal apps.", SecurityException.class,
135                 mImManager::addVirtualStylusIdForTestSession);
136     }
137 
138     /**
139      * Verifies that the test API {@link InputMethodManager#setStylusWindowIdleTimeoutForTest(long)}
140      * is properly protected with some permission.
141      */
142     @Test
testSetStylusWindowIdleTimeoutForTestProtection()143     public void testSetStylusWindowIdleTimeoutForTestProtection() {
144         assumeTrue(mContext.getPackageManager().hasSystemFeature(FEATURE_INPUT_METHODS));
145 
146         assertThrows("InputMethodManager#setStylusWindowIdleTimeoutForTest(long) must not"
147                         + " be accessible to normal apps.", SecurityException.class,
148                 () -> mImManager.setStylusWindowIdleTimeoutForTest(0));
149     }
150 
151     @Test
testIsActive()152     public void testIsActive() throws Throwable {
153         final AtomicReference<EditText> focusedEditTextRef = new AtomicReference<>();
154         final AtomicReference<EditText> nonFocusedEditTextRef = new AtomicReference<>();
155         TestActivity.startSync(activity -> {
156             final LinearLayout layout = new LinearLayout(activity);
157             layout.setOrientation(LinearLayout.VERTICAL);
158 
159             final EditText focusedEditText = new EditText(activity);
160             layout.addView(focusedEditText);
161             focusedEditTextRef.set(focusedEditText);
162             focusedEditText.requestFocus();
163 
164             final EditText nonFocusedEditText = new EditText(activity);
165             layout.addView(nonFocusedEditText);
166             nonFocusedEditTextRef.set(nonFocusedEditText);
167 
168             return layout;
169         });
170         final View focusedEditText = focusedEditTextRef.get();
171         waitOnMainUntil(() -> mImManager.hasActiveInputConnection(focusedEditText), TIMEOUT);
172         assertTrue(getOnMainSync(() -> mImManager.isActive(focusedEditText)));
173         assertFalse(getOnMainSync(() -> mImManager.isActive(nonFocusedEditTextRef.get())));
174     }
175 
176     @Test
testIsAcceptingText()177     public void testIsAcceptingText() throws Throwable {
178         final AtomicReference<EditText> focusedFakeEditTextRef = new AtomicReference<>();
179         final CountDownLatch latch = new CountDownLatch(1);
180         TestActivity.startSync(activity -> {
181             final LinearLayout layout = new LinearLayout(activity);
182             layout.setOrientation(LinearLayout.VERTICAL);
183 
184             final EditText focusedFakeEditText = new EditText(activity) {
185                 @Override
186                 public InputConnection onCreateInputConnection(EditorInfo info) {
187                     super.onCreateInputConnection(info);
188                     latch.countDown();
189                     return null;
190                 }
191             };
192             layout.addView(focusedFakeEditText);
193             focusedFakeEditTextRef.set(focusedFakeEditText);
194             focusedFakeEditText.requestFocus();
195             return layout;
196         });
197         assertTrue(latch.await(TIMEOUT, TimeUnit.MILLISECONDS));
198         assertFalse("InputMethodManager#isAcceptingText() must return false "
199                 + "if target View returns null from onCreateInputConnection().",
200                 getOnMainSync(() -> mImManager.isAcceptingText()));
201     }
202 
203     @Test
testGetInputMethodList()204     public void testGetInputMethodList() throws Exception {
205         final List<InputMethodInfo> enabledImes = mImManager.getEnabledInputMethodList();
206         assertNotNull(enabledImes);
207         final List<InputMethodInfo> imes = mImManager.getInputMethodList();
208         assertNotNull(imes);
209 
210         // Make sure that IMM#getEnabledInputMethodList() is a subset of IMM#getInputMethodList().
211         // TODO: Consider moving this to hostside test to test more realistic and useful scenario.
212         if (!imes.containsAll(enabledImes)) {
213             fail("Enabled IMEs must be a subset of all the IMEs.\n"
214                     + "all=" + dumpInputMethodInfoList(imes) + "\n"
215                     + "enabled=" + dumpInputMethodInfoList(enabledImes));
216         }
217     }
218 
219     @Test
testGetEnabledInputMethodList()220     public void testGetEnabledInputMethodList() throws Exception {
221         enableImes(HIDDEN_FROM_PICKER_IME_ID);
222         final List<InputMethodInfo> enabledImes = mImManager.getEnabledInputMethodList();
223         assertThat(enabledImes).isNotNull();
224         final List<String> enabledImeIds =
225                 enabledImes.stream().map(InputMethodInfo::getId).collect(Collectors.toList());
226         assertThat(enabledImeIds).contains(HIDDEN_FROM_PICKER_IME_ID);
227     }
228 
dumpInputMethodInfoList(@onNull List<InputMethodInfo> imiList)229     private static String dumpInputMethodInfoList(@NonNull List<InputMethodInfo> imiList) {
230         return "[" + imiList.stream().map(imi -> {
231             final StringBuilder sb = new StringBuilder();
232             final int subtypeCount = imi.getSubtypeCount();
233             sb.append("InputMethodInfo{id=").append(imi.getId())
234                     .append(", subtypeCount=").append(subtypeCount)
235                     .append(", subtypes=[");
236             for (int i = 0; i < subtypeCount; ++i) {
237                 if (i != 0) {
238                     sb.append(",");
239                 }
240                 final InputMethodSubtype subtype = imi.getSubtypeAt(i);
241                 sb.append("{id=0x").append(Integer.toHexString(subtype.hashCode()));
242                 if (!TextUtils.isEmpty(subtype.getMode())) {
243                     sb.append(",mode=").append(subtype.getMode());
244                 }
245                 if (!TextUtils.isEmpty(subtype.getLocale())) {
246                     sb.append(",locale=").append(subtype.getLocale());
247                 }
248                 if (!TextUtils.isEmpty(subtype.getLanguageTag())) {
249                     sb.append(",languageTag=").append(subtype.getLanguageTag());
250                 }
251                 sb.append("}");
252             }
253             sb.append("]");
254             return sb.toString();
255         }).collect(Collectors.joining(", ")) + "]";
256     }
257 
258     @AppModeFull(reason = "Instant apps cannot rely on ACTION_CLOSE_SYSTEM_DIALOGS")
259     @Test
testShowInputMethodPicker()260     public void testShowInputMethodPicker() throws Exception {
261         assumeTrue(mContext.getPackageManager().hasSystemFeature(
262                 PackageManager.FEATURE_INPUT_METHODS));
263         enableImes(MOCK_IME_ID, HIDDEN_FROM_PICKER_IME_ID);
264 
265         TestActivity.startSync(activity -> {
266             final View view = new View(activity);
267             view.setLayoutParams(new LayoutParams(
268                     LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
269             return view;
270         });
271 
272         // Make sure that InputMethodPicker is not shown in the initial state.
273         mContext.sendBroadcast(
274                 new Intent(ACTION_CLOSE_SYSTEM_DIALOGS).setFlags(FLAG_RECEIVER_FOREGROUND));
275         waitOnMainUntil(() -> !isInputMethodPickerShown(mImManager), TIMEOUT,
276                 "InputMethod picker should be closed");
277 
278         // Test InputMethodManager#showInputMethodPicker() works as expected.
279         mImManager.showInputMethodPicker();
280         waitOnMainUntil(() -> isInputMethodPickerShown(mImManager), TIMEOUT,
281                 "InputMethod picker should be shown");
282 
283         // UiDevice.getInstance(Instrumentation) may return a cached instance if it's already called
284         // in this process and for some unknown reasons it fails to detect MOCK_IME_LABEL.
285         // As a quick workaround, here we clear its internal singleton value.
286         // TODO(b/230698095): Fix this in UiDevice or stop using UiDevice.
287         try {
288             final Field field = UiDevice.class.getDeclaredField("sInstance");
289             field.setAccessible(true);
290             field.set(null, null);
291         } catch (NoSuchFieldException | SecurityException | IllegalArgumentException
292                 | IllegalAccessException e) {
293             // We don't treat this as an error as it's an implementation detail of UiDevice.
294         }
295 
296         final UiDevice uiDevice = UiDevice.getInstance(mInstrumentation);
297         assertThat(uiDevice.wait(Until.hasObject(By.text(MOCK_IME_LABEL)), TIMEOUT)).isTrue();
298         assertThat(uiDevice.findObject(By.text(HIDDEN_FROM_PICKER_IME_LABEL))).isNull();
299 
300         // Make sure that InputMethodPicker can be closed with ACTION_CLOSE_SYSTEM_DIALOGS
301         mContext.sendBroadcast(
302                 new Intent(ACTION_CLOSE_SYSTEM_DIALOGS).setFlags(FLAG_RECEIVER_FOREGROUND));
303         waitOnMainUntil(() -> !isInputMethodPickerShown(mImManager), TIMEOUT,
304                 "InputMethod picker should be closed");
305     }
306 
307     @Test
testNoStrongServedViewReferenceAfterWindowDetached()308     public void testNoStrongServedViewReferenceAfterWindowDetached() throws IOException {
309         var receivedSignalCleaned = new CountDownLatch(1);
310         Runnable r = () -> {
311             var viewRef = new View[1];
312             TestActivity testActivity = TestActivity.startSync(activity -> {
313                 viewRef[0] = new EditText(activity);
314                 viewRef[0].setLayoutParams(new LayoutParams(
315                         LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
316                 viewRef[0].requestFocus();
317                 return viewRef[0];
318             });
319             // wait until editText becomes active
320             final InputMethodManager imm = testActivity.getSystemService(InputMethodManager.class);
321             PollingCheck.waitFor(() -> imm.hasActiveInputConnection(viewRef[0]));
322 
323             Cleaner.create().register(viewRef[0], receivedSignalCleaned::countDown);
324             viewRef[0] = null;
325 
326             // finishing the activity should destroy the reference inside IMM
327             testActivity.finish();
328         };
329         r.run();
330 
331         waitForWithGc(() -> receivedSignalCleaned.getCount() == 0);
332     }
333 
waitForWithGc(PollingCheck.PollingCheckCondition condition)334     private void waitForWithGc(PollingCheck.PollingCheckCondition condition) throws IOException {
335         try {
336             PollingCheck.waitFor(() -> {
337                 Runtime.getRuntime().gc();
338                 return condition.canProceed();
339             });
340         } catch (AssertionError e) {
341             var dir = new File("/sdcard/DumpOnFailure");
342             if (!dir.exists()) {
343                 assertTrue("Unable to create " + dir, dir.mkdir());
344             }
345             File heap = new File(dir, "inputmethod-dump.hprof");
346             Debug.dumpHprofData(heap.getAbsolutePath());
347             throw new AssertionError("Dumped heap in device at " + heap.getAbsolutePath(), e);
348         }
349     }
350 
enableImes(String... ids)351     private void enableImes(String... ids) {
352         for (String id : ids) {
353             runShellCommandOrThrow("ime enable " + id);
354         }
355         mNeedsImeReset = true;
356     }
357 }
358