1 /* 2 * Copyright (C) 2021 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.view.inputmethod.cts.util.TestUtils.runOnMainSync; 20 21 import static com.android.cts.mockime.ImeEventStreamTestUtils.editorMatcher; 22 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectCommand; 23 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent; 24 import static com.android.cts.mockime.ImeEventStreamTestUtils.hideSoftInputMatcher; 25 import static com.android.cts.mockime.ImeEventStreamTestUtils.notExpectEvent; 26 import static com.android.cts.mockime.ImeEventStreamTestUtils.showSoftInputMatcher; 27 28 import static com.google.common.truth.Truth.assertThat; 29 30 import static org.junit.Assert.assertEquals; 31 import static org.junit.Assert.assertFalse; 32 import static org.junit.Assert.assertNotEquals; 33 import static org.junit.Assert.assertNotNull; 34 import static org.junit.Assert.assertTrue; 35 import static org.junit.Assert.fail; 36 37 import android.graphics.Color; 38 import android.platform.test.annotations.AppModeSdkSandbox; 39 import android.view.View; 40 import android.view.ViewGroup; 41 import android.view.inputmethod.EditorInfo; 42 import android.view.inputmethod.InputConnection; 43 import android.view.inputmethod.InputMethodManager; 44 import android.view.inputmethod.SurroundingText; 45 import android.view.inputmethod.cts.util.EndToEndImeTestBase; 46 import android.view.inputmethod.cts.util.TestActivity; 47 import android.view.inputmethod.cts.util.UnlockScreenRule; 48 import android.widget.EditText; 49 import android.widget.LinearLayout; 50 51 import androidx.annotation.NonNull; 52 import androidx.annotation.Nullable; 53 import androidx.test.filters.MediumTest; 54 import androidx.test.platform.app.InstrumentationRegistry; 55 56 import com.android.cts.mockime.ImeEvent; 57 import com.android.cts.mockime.ImeEventStream; 58 import com.android.cts.mockime.ImeSettings; 59 import com.android.cts.mockime.MockImeSession; 60 61 import org.junit.Rule; 62 import org.junit.Test; 63 64 import java.util.concurrent.TimeUnit; 65 import java.util.concurrent.atomic.AtomicReference; 66 67 /** 68 * Contains test cases for some special IME-related behaviors of {@link EditText}. 69 */ 70 @MediumTest 71 @AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).") 72 public final class EditTextImeSupportTest extends EndToEndImeTestBase { 73 private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5); 74 private static final long NOT_EXPECT_TIMEOUT = 10; // msec 75 76 @Rule 77 public final UnlockScreenRule mUnlockScreenRule = new UnlockScreenRule(); 78 launchTestActivity(String marker, String initialText, int initialSelectionStart, int initialSelectionEnd)79 public EditText launchTestActivity(String marker, String initialText, 80 int initialSelectionStart, int initialSelectionEnd) { 81 final AtomicReference<EditText> editTextRef = new AtomicReference<>(); 82 TestActivity.startSync(activity-> { 83 final LinearLayout layout = new LinearLayout(activity); 84 layout.setOrientation(LinearLayout.VERTICAL); 85 86 final EditText editText = new EditText(activity); 87 editText.setPrivateImeOptions(marker); 88 editText.setHint("editText"); 89 editText.setText(initialText); 90 editText.setSelection(initialSelectionStart, initialSelectionEnd); 91 editText.requestFocus(); 92 editTextRef.set(editText); 93 94 layout.addView(editText); 95 return layout; 96 }); 97 return editTextRef.get(); 98 } 99 100 /** 101 * A regression test for Bug 161330778. 102 */ 103 @Test testSetTextTriggersRestartInput()104 public void testSetTextTriggersRestartInput() throws Exception { 105 try (MockImeSession imeSession = MockImeSession.create( 106 InstrumentationRegistry.getInstrumentation().getContext(), 107 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 108 new ImeSettings.Builder())) { 109 final ImeEventStream stream = imeSession.openEventStream(); 110 111 final String initialText = "0123456789"; 112 final int initialSelectionStart = 3; 113 final int initialSelectionEnd = 7; 114 final String marker = getTestMarker(); 115 final EditText editText = launchTestActivity(marker, initialText, initialSelectionStart, 116 initialSelectionEnd); 117 118 // 1nd onStartInput() should be with restarting == false, with the correct initial 119 // surrounding text information. 120 final ImeEvent firstOnStartInput = expectEvent(stream, 121 editorMatcher("onStartInput", marker), TIMEOUT); 122 assertFalse(firstOnStartInput.getArguments().getBoolean("restarting")); 123 124 // Verify the initial surrounding text info. 125 final EditorInfo initialEditorInfo = 126 firstOnStartInput.getArguments().getParcelable("editorInfo"); 127 assertNotNull(initialEditorInfo); 128 assertInitialSurroundingText(initialEditorInfo, initialText, initialSelectionStart, 129 initialSelectionEnd); 130 131 final String updatedText = "NewText"; 132 133 // Create a copy of the stream to verify that there is no onUpdateSelection(). 134 stream.skipAll(); 135 final ImeEventStream copiedStream = stream.copy(); 136 137 // This should trigger InputMethodManager#restartInput(), which triggers the 2nd 138 // onStartInput() with restarting == true. 139 runOnMainSync(() -> editText.setText(updatedText)); 140 final ImeEvent secondOnStartInput = expectEvent(stream, 141 editorMatcher("onStartInput", marker), TIMEOUT); 142 assertTrue(secondOnStartInput.getArguments().getBoolean("restarting")); 143 144 // Verify the initial surrounding text after TextView#setText(). The cursor must be 145 // placed at the beginning of the new text. 146 final EditorInfo restartingEditorInfo = 147 secondOnStartInput.getArguments().getParcelable("editorInfo"); 148 assertNotNull(restartingEditorInfo); 149 assertInitialSurroundingText(restartingEditorInfo, updatedText, 0, 0); 150 151 assertFalse("TextView#setText() must not trigger onUpdateSelection", 152 copiedStream.findFirst( 153 event -> "onUpdateSelection".equals(event.getEventName())).isPresent()); 154 } 155 } 156 assertInitialSurroundingText(@onNull EditorInfo editorInfo, @NonNull String expectedText, int expectedSelectionStart, int expectedSelectionEnd)157 private static void assertInitialSurroundingText(@NonNull EditorInfo editorInfo, 158 @NonNull String expectedText, int expectedSelectionStart, int expectedSelectionEnd) { 159 160 assertNotEquals("expectedText must has a selection", -1, expectedSelectionStart); 161 assertNotEquals("expectedText must has a selection", -1, expectedSelectionEnd); 162 163 final CharSequence expectedTextBeforeCursor = 164 expectedText.subSequence(0, expectedSelectionStart); 165 final CharSequence expectedSelectedText = 166 expectedText.subSequence(expectedSelectionStart, expectedSelectionEnd); 167 final CharSequence expectedTextAfterCursor = 168 expectedText.subSequence(expectedSelectionEnd, expectedText.length()); 169 final int expectedTextLength = expectedText.length(); 170 171 assertEqualsWithIgnoringSpans(expectedTextBeforeCursor, 172 editorInfo.getInitialTextBeforeCursor(expectedTextLength, 0)); 173 assertEqualsWithIgnoringSpans(expectedSelectedText, 174 editorInfo.getInitialSelectedText(0)); 175 assertEqualsWithIgnoringSpans(expectedTextAfterCursor, 176 editorInfo.getInitialTextAfterCursor(expectedTextLength, 0)); 177 178 final SurroundingText initialSurroundingText = 179 editorInfo.getInitialSurroundingText(expectedTextLength, expectedTextLength, 0); 180 assertNotNull(initialSurroundingText); 181 assertEqualsWithIgnoringSpans(expectedText, initialSurroundingText.getText()); 182 assertEquals(expectedSelectionStart, initialSurroundingText.getSelectionStart()); 183 assertEquals(expectedSelectionEnd, initialSurroundingText.getSelectionEnd()); 184 } 185 assertEqualsWithIgnoringSpans(@ullable CharSequence expected, @Nullable CharSequence actual)186 private static void assertEqualsWithIgnoringSpans(@Nullable CharSequence expected, 187 @Nullable CharSequence actual) { 188 if (expected == actual) { 189 return; 190 } 191 if (expected == null) { 192 fail("must be null but was " + actual); 193 } 194 if (actual == null) { 195 fail("must be " + expected + " but was null"); 196 } 197 assertEquals(expected.toString(), actual.toString()); 198 } 199 200 /** 201 * Test when to see {@link EditorInfo#IME_FLAG_NAVIGATE_NEXT} and 202 * {@link EditorInfo#IME_FLAG_NAVIGATE_PREVIOUS}. 203 * 204 * <p>This is also a regression test for Bug 31099943.</p> 205 */ 206 @Test testNavigateFlags()207 public void testNavigateFlags() throws Exception { 208 try (MockImeSession imeSession = MockImeSession.create( 209 InstrumentationRegistry.getInstrumentation().getContext(), 210 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 211 new ImeSettings.Builder())) { 212 final ImeEventStream stream = imeSession.openEventStream(); 213 214 // For a single EditText, there should be no navigate flag 215 verifyNavigateFlags(stream, new Control[]{ 216 Control.FOCUSED_EDIT_TEXT, 217 }, false /* navigateNext */, false /* navigatePrevious */); 218 219 // For two EditText controls, there should be one navigate flag depending on the 220 // geometry. 221 verifyNavigateFlags(stream, new Control[]{ 222 Control.FOCUSED_EDIT_TEXT, 223 Control.EDIT_TEXT, 224 }, true /* navigateNext */, false /* navigatePrevious */); 225 verifyNavigateFlags(stream, new Control[]{ 226 Control.EDIT_TEXT, 227 Control.FOCUSED_EDIT_TEXT, 228 }, false /* navigateNext */, true /* navigatePrevious */); 229 230 // Non focusable View controls should be ignored when determining navigation flags. 231 verifyNavigateFlags(stream, new Control[]{ 232 Control.NON_FOCUSABLE_VIEW, 233 Control.FOCUSED_EDIT_TEXT, 234 Control.NON_FOCUSABLE_VIEW, 235 }, false /* navigateNext */, false /* navigatePrevious */); 236 237 // Even focusable View controls should be ignored when determining navigation flags if 238 // View#onCheckIsTextEditor() returns false. (Regression test for Bug 31099943) 239 verifyNavigateFlags(stream, new Control[]{ 240 Control.FOCUSABLE_VIEW, 241 Control.FOCUSED_EDIT_TEXT, 242 Control.FOCUSABLE_VIEW, 243 }, false /* navigateNext */, false /* navigatePrevious */); 244 } 245 } 246 247 private enum Control { 248 EDIT_TEXT, 249 FOCUSED_EDIT_TEXT, 250 FOCUSABLE_VIEW, 251 NON_FOCUSABLE_VIEW, 252 } 253 verifyNavigateFlags(@onNull ImeEventStream stream, @NonNull Control[] controls, boolean navigateNext, boolean navigatePrevious)254 private void verifyNavigateFlags(@NonNull ImeEventStream stream, @NonNull Control[] controls, 255 boolean navigateNext, boolean navigatePrevious) throws Exception { 256 final String marker = getTestMarker(); 257 TestActivity.startSync(activity-> { 258 final LinearLayout layout = new LinearLayout(activity); 259 layout.setOrientation(LinearLayout.VERTICAL); 260 for (Control control : controls) { 261 switch (control) { 262 case EDIT_TEXT: 263 case FOCUSED_EDIT_TEXT: { 264 final boolean focused = (Control.FOCUSED_EDIT_TEXT == control); 265 final EditText editText = new EditText(activity); 266 editText.setHint("editText"); 267 layout.addView(editText); 268 if (focused) { 269 editText.setPrivateImeOptions(marker); 270 editText.requestFocus(); 271 } 272 break; 273 } 274 case FOCUSABLE_VIEW: 275 case NON_FOCUSABLE_VIEW: { 276 final boolean focusable = (Control.FOCUSABLE_VIEW == control); 277 final View view = new View(activity); 278 view.setBackgroundColor(focusable ? Color.YELLOW : Color.RED); 279 view.setFocusable(focusable); 280 view.setFocusableInTouchMode(focusable); 281 view.setLayoutParams(new ViewGroup.LayoutParams( 282 ViewGroup.LayoutParams.MATCH_PARENT, 10 /* height */)); 283 layout.addView(view); 284 break; 285 } 286 default: 287 throw new UnsupportedOperationException("Unknown control=" + control); 288 } 289 } 290 return layout; 291 }); 292 293 final ImeEvent startInput = expectEvent(stream, 294 editorMatcher("onStartInput", marker), TIMEOUT); 295 final EditorInfo editorInfo = startInput.getArguments().getParcelable("editorInfo"); 296 assertThat(editorInfo).isNotNull(); 297 assertThat(editorInfo.imeOptions & EditorInfo.IME_FLAG_NAVIGATE_NEXT) 298 .isEqualTo(navigateNext ? EditorInfo.IME_FLAG_NAVIGATE_NEXT : 0); 299 assertThat(editorInfo.imeOptions & EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS) 300 .isEqualTo(navigatePrevious ? EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS : 0); 301 } 302 303 /** 304 * Regression test for Bug 209958658. 305 */ 306 @Test testEndBatchEditReturnValue()307 public void testEndBatchEditReturnValue() { 308 EditText editText = new EditText(InstrumentationRegistry.getInstrumentation().getContext()); 309 EditorInfo editorInfo = new EditorInfo(); 310 InputConnection editableInputConnection = editText.onCreateInputConnection(editorInfo); 311 assertThat(editableInputConnection.beginBatchEdit()).isTrue(); 312 assertThat(editableInputConnection.beginBatchEdit()).isTrue(); 313 assertThat(editableInputConnection.endBatchEdit()).isTrue(); 314 assertThat(editableInputConnection.endBatchEdit()).isFalse(); 315 316 // Extra invocations of endBatchEdit() continue to return false. 317 assertThat(editableInputConnection.endBatchEdit()).isFalse(); 318 } 319 320 /** 321 * Verifies that IME receives a hide request when an active {@link EditText} becomes 322 * disabled. 323 */ 324 @Test testHideSoftInputWhenDisabled()325 public void testHideSoftInputWhenDisabled() throws Exception { 326 try (MockImeSession imeSession = MockImeSession.create( 327 InstrumentationRegistry.getInstrumentation().getContext(), 328 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 329 new ImeSettings.Builder())) { 330 final ImeEventStream stream = imeSession.openEventStream(); 331 332 final String marker = getTestMarker(); 333 final AtomicReference<EditText> editTextRef = new AtomicReference<>(); 334 TestActivity.startSync(activity-> { 335 final LinearLayout layout = new LinearLayout(activity); 336 layout.setOrientation(LinearLayout.VERTICAL); 337 338 final EditText editText = new EditText(activity); 339 editText.setPrivateImeOptions(marker); 340 editText.requestFocus(); 341 editTextRef.set(editText); 342 343 layout.addView(editText); 344 return layout; 345 }); 346 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 347 348 final var editText = editTextRef.get(); 349 runOnMainSync(() -> editText.getContext().getSystemService(InputMethodManager.class) 350 .showSoftInput(editText, 0)); 351 expectEvent(stream, showSoftInputMatcher(0), TIMEOUT); 352 353 runOnMainSync(() -> editText.setEnabled(false)); 354 expectEvent(stream, hideSoftInputMatcher(), TIMEOUT); 355 } 356 } 357 358 /** 359 * Verifies that IME receives a hide request when an active {@link EditText} receives 360 * {@link EditorInfo#IME_ACTION_DONE}. 361 */ 362 @Test testHideSoftInputByActionDone()363 public void testHideSoftInputByActionDone() throws Exception { 364 try (MockImeSession imeSession = MockImeSession.create( 365 InstrumentationRegistry.getInstrumentation().getContext(), 366 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 367 new ImeSettings.Builder())) { 368 final ImeEventStream stream = imeSession.openEventStream(); 369 370 final String marker = getTestMarker(); 371 final AtomicReference<EditText> editTextRef = new AtomicReference<>(); 372 TestActivity.startSync(activity-> { 373 final LinearLayout layout = new LinearLayout(activity); 374 layout.setOrientation(LinearLayout.VERTICAL); 375 376 final EditText editText = new EditText(activity); 377 editText.setPrivateImeOptions(marker); 378 editText.requestFocus(); 379 editTextRef.set(editText); 380 381 layout.addView(editText); 382 return layout; 383 }); 384 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 385 386 final var editText = editTextRef.get(); 387 runOnMainSync(() -> editText.getContext().getSystemService(InputMethodManager.class) 388 .showSoftInput(editText, 0)); 389 expectEvent(stream, showSoftInputMatcher(0), TIMEOUT); 390 391 expectCommand(stream, imeSession.callPerformEditorAction(EditorInfo.IME_ACTION_DONE), 392 TIMEOUT); 393 expectEvent(stream, hideSoftInputMatcher(), TIMEOUT); 394 } 395 } 396 397 /** 398 * Verifies that disabling an {@link EditText} after its losing input focus will not hide the 399 * software keyboard. 400 * 401 * <p>This is a simplified repro code of Bug 332912075.</p> 402 */ 403 @Test testDisableImeAfterFocusChange()404 public void testDisableImeAfterFocusChange() throws Exception { 405 try (MockImeSession imeSession = MockImeSession.create( 406 InstrumentationRegistry.getInstrumentation().getContext(), 407 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 408 new ImeSettings.Builder())) { 409 final ImeEventStream stream = imeSession.openEventStream(); 410 411 final String marker1 = getTestMarker("EditText1"); 412 final String marker2 = getTestMarker("EditText2"); 413 final AtomicReference<EditText> editTextRef1 = new AtomicReference<>(); 414 final AtomicReference<EditText> editTextRef2 = new AtomicReference<>(); 415 TestActivity.startSync(activity-> { 416 final LinearLayout layout = new LinearLayout(activity); 417 layout.setOrientation(LinearLayout.VERTICAL); 418 { 419 final EditText editText = new EditText(activity); 420 editText.setPrivateImeOptions(marker1); 421 editText.requestFocus(); 422 editTextRef1.set(editText); 423 layout.addView(editText); 424 } 425 { 426 final EditText editText = new EditText(activity); 427 editText.setPrivateImeOptions(marker2); 428 editTextRef2.set(editText); 429 layout.addView(editText); 430 } 431 return layout; 432 }); 433 final var editText1 = editTextRef1.get(); 434 final var editText2 = editTextRef2.get(); 435 436 expectEvent(stream, editorMatcher("onStartInput", marker1), TIMEOUT); 437 notExpectEvent(stream, showSoftInputMatcher(0), NOT_EXPECT_TIMEOUT); 438 439 // Make sure to show the IME. 440 runOnMainSync(() -> editText1.getContext().getSystemService(InputMethodManager.class) 441 .showSoftInput(editText1, 0)); 442 expectEvent(stream, showSoftInputMatcher(0), TIMEOUT); 443 444 runOnMainSync(() -> { 445 editText2.requestFocus(); 446 editText1.setEnabled(false); 447 }); 448 449 var forkedStream = stream.copy(); 450 expectEvent(stream, editorMatcher("onStartInput", marker2), TIMEOUT); 451 notExpectEvent(forkedStream, hideSoftInputMatcher(), NOT_EXPECT_TIMEOUT); 452 } 453 } 454 } 455