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