1 /* 2 * Copyright (C) 2018 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.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; 20 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; 21 import static android.inputmethodservice.InputMethodService.DISALLOW_INPUT_METHOD_INTERFACE_OVERRIDE; 22 import static android.server.wm.jetpack.extensions.util.ExtensionsUtil.assumeExtensionSupportedDevice; 23 import static android.view.WindowInsets.Type.ime; 24 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN; 25 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE; 26 import static android.view.inputmethod.cts.util.ConstantsUtils.DISAPPROVE_IME_PACKAGE_NAME; 27 import static android.view.inputmethod.cts.util.InputMethodVisibilityVerifier.expectImeInvisible; 28 import static android.view.inputmethod.cts.util.InputMethodVisibilityVerifier.expectImeVisible; 29 import static android.view.inputmethod.cts.util.TestUtils.getOnMainSync; 30 import static android.view.inputmethod.cts.util.TestUtils.runOnMainSync; 31 import static android.view.inputmethod.cts.util.TestUtils.waitOnMainUntil; 32 33 import static com.android.cts.mockime.ImeEventStreamTestUtils.EventFilterMode.CHECK_EXIT_EVENT_ONLY; 34 import static com.android.cts.mockime.ImeEventStreamTestUtils.WindowLayoutInfoParcelable; 35 import static com.android.cts.mockime.ImeEventStreamTestUtils.editorMatcher; 36 import static com.android.cts.mockime.ImeEventStreamTestUtils.eventMatcher; 37 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectCommand; 38 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent; 39 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEventWithKeyValue; 40 import static com.android.cts.mockime.ImeEventStreamTestUtils.notExpectEvent; 41 import static com.android.cts.mockime.ImeEventStreamTestUtils.showSoftInputMatcher; 42 import static com.android.cts.mockime.ImeEventStreamTestUtils.verificationMatcher; 43 import static com.android.cts.mockime.ImeEventStreamTestUtils.withDescription; 44 45 import static com.google.common.truth.Truth.assertThat; 46 import static com.google.common.truth.Truth.assertWithMessage; 47 import static com.google.common.truth.TruthJUnit.assume; 48 49 import static org.junit.Assert.assertEquals; 50 import static org.junit.Assert.assertNotNull; 51 import static org.junit.Assert.assertTrue; 52 import static org.junit.Assert.fail; 53 import static org.junit.Assume.assumeTrue; 54 55 import android.app.Activity; 56 import android.app.Instrumentation; 57 import android.content.Context; 58 import android.content.Intent; 59 import android.content.pm.PackageManager; 60 import android.graphics.Matrix; 61 import android.graphics.Rect; 62 import android.graphics.RectF; 63 import android.inputmethodservice.InputMethodService; 64 import android.os.Bundle; 65 import android.os.SystemClock; 66 import android.platform.test.annotations.AppModeSdkSandbox; 67 import android.server.wm.DisplayMetricsSession; 68 import android.support.test.uiautomator.UiObject2; 69 import android.text.TextUtils; 70 import android.view.Display; 71 import android.view.KeyCharacterMap; 72 import android.view.KeyEvent; 73 import android.view.View; 74 import android.view.inputmethod.CursorAnchorInfo; 75 import android.view.inputmethod.EditorBoundsInfo; 76 import android.view.inputmethod.EditorInfo; 77 import android.view.inputmethod.InputConnection; 78 import android.view.inputmethod.InputConnectionWrapper; 79 import android.view.inputmethod.InputMethodInfo; 80 import android.view.inputmethod.InputMethodManager; 81 import android.view.inputmethod.TextAppearanceInfo; 82 import android.view.inputmethod.cts.disapproveime.DisapproveInputMethodService; 83 import android.view.inputmethod.cts.util.EndToEndImeTestBase; 84 import android.view.inputmethod.cts.util.TestActivity; 85 import android.view.inputmethod.cts.util.TestActivity2; 86 import android.view.inputmethod.cts.util.TestUtils; 87 import android.view.inputmethod.cts.util.TestWebView; 88 import android.view.inputmethod.cts.util.UnlockScreenRule; 89 import android.webkit.WebView; 90 import android.widget.EditText; 91 import android.widget.LinearLayout; 92 93 import androidx.annotation.NonNull; 94 import androidx.test.filters.FlakyTest; 95 import androidx.test.filters.MediumTest; 96 import androidx.test.platform.app.InstrumentationRegistry; 97 import androidx.test.rule.ServiceTestRule; 98 import androidx.test.runner.AndroidJUnit4; 99 import androidx.window.extensions.layout.DisplayFeature; 100 import androidx.window.extensions.layout.WindowLayoutInfo; 101 102 import com.android.compatibility.common.util.ApiTest; 103 import com.android.compatibility.common.util.PollingCheck; 104 import com.android.compatibility.common.util.SystemUtil; 105 import com.android.cts.mockime.ImeCommand; 106 import com.android.cts.mockime.ImeEvent; 107 import com.android.cts.mockime.ImeEventStream; 108 import com.android.cts.mockime.ImeEventStreamTestUtils.DescribedPredicate; 109 import com.android.cts.mockime.ImeSettings; 110 import com.android.cts.mockime.MockImeSession; 111 112 import org.junit.Assume; 113 import org.junit.Before; 114 import org.junit.Rule; 115 import org.junit.Test; 116 import org.junit.runner.RunWith; 117 118 import java.io.IOException; 119 import java.util.ArrayList; 120 import java.util.List; 121 import java.util.concurrent.CountDownLatch; 122 import java.util.concurrent.TimeUnit; 123 import java.util.concurrent.TimeoutException; 124 import java.util.concurrent.atomic.AtomicInteger; 125 import java.util.concurrent.atomic.AtomicReference; 126 import java.util.function.Function; 127 128 /** 129 * Tests for {@link InputMethodService} methods. 130 * 131 * Build/Install/Run: 132 * atest CtsInputMethodTestCases:InputMethodServiceTest 133 */ 134 @MediumTest 135 @RunWith(AndroidJUnit4.class) 136 @AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).") 137 public class InputMethodServiceTest extends EndToEndImeTestBase { 138 private static final String TAG = "InputMethodServiceTest"; 139 private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(20); 140 private static final long EXPECTED_TIMEOUT = TimeUnit.SECONDS.toMillis(2); 141 private static final long ACTIVITY_LAUNCH_INTERVAL = 500; // msec 142 143 private static final String OTHER_IME_ID = "com.android.cts.spellcheckingime/.SpellCheckingIme"; 144 145 private static final String ERASE_FONT_SCALE_CMD = "settings delete system font_scale"; 146 // 1.2 is an arbitrary value. 147 private static final String PUT_FONT_SCALE_CMD = "settings put system font_scale 1.2"; 148 149 @Rule 150 public final UnlockScreenRule mUnlockScreenRule = new UnlockScreenRule(); 151 @Rule 152 public final ServiceTestRule mServiceRule = new ServiceTestRule(); 153 154 private final String mMarker = getTestMarker(); 155 156 private Instrumentation mInstrumentation; 157 backKeyDownMatcher(boolean expectedReturnValue)158 private static DescribedPredicate<ImeEvent> backKeyDownMatcher(boolean expectedReturnValue) { 159 return withDescription("onKeyDown(KEYCODE_BACK) = " + expectedReturnValue, event -> { 160 if (!TextUtils.equals("onKeyDown", event.getEventName())) { 161 return false; 162 } 163 final int keyCode = event.getArguments().getInt("keyCode"); 164 if (keyCode != KeyEvent.KEYCODE_BACK) { 165 return false; 166 } 167 return event.getReturnBooleanValue() == expectedReturnValue; 168 }); 169 } 170 171 @Before 172 public void setup() { 173 mInstrumentation = InstrumentationRegistry.getInstrumentation(); 174 } 175 176 private TestActivity createTestActivity(int windowFlags) { 177 return TestActivity.startSync(activity -> createLayout(windowFlags, activity)); 178 } 179 180 private TestActivity createTestActivity(int windowFlags, int displayId) { 181 return new TestActivity.Starter().withDisplayId(displayId).startSync( 182 activity -> createLayout(windowFlags, activity), TestActivity.class); 183 } 184 185 private TestActivity createTestActivity2(int windowFlags) { 186 return new TestActivity.Starter().startSync(activity -> createLayout(windowFlags, activity), 187 TestActivity2.class); 188 } 189 190 private LinearLayout createLayout(final int windowFlags, final Activity activity) { 191 final LinearLayout layout = new LinearLayout(activity); 192 layout.setOrientation(LinearLayout.VERTICAL); 193 194 final EditText editText = new EditText(activity); 195 editText.setText("Editable"); 196 editText.setPrivateImeOptions(mMarker); 197 layout.addView(editText); 198 editText.requestFocus(); 199 200 activity.getWindow().setSoftInputMode(windowFlags); 201 return layout; 202 } 203 204 205 @Test 206 public void verifyLayoutInflaterContext() throws Exception { 207 try (MockImeSession imeSession = MockImeSession.create( 208 InstrumentationRegistry.getInstrumentation().getContext(), 209 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 210 new ImeSettings.Builder())) { 211 final ImeEventStream stream = imeSession.openEventStream(); 212 213 createTestActivity(SOFT_INPUT_STATE_ALWAYS_VISIBLE); 214 expectEvent(stream, editorMatcher("onStartInputView", mMarker), TIMEOUT); 215 216 final ImeCommand command = imeSession.verifyLayoutInflaterContext(); 217 assertTrue("InputMethodService.getLayoutInflater().getContext() must be equal to" 218 + " InputMethodService.this", 219 expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); 220 } 221 } 222 223 @Test 224 public void testSwitchInputMethod_verifiesEnabledState() throws Exception { 225 Assume.assumeFalse(isPreventImeStartup()); 226 SystemUtil.runShellCommandOrThrow("ime disable " + OTHER_IME_ID); 227 try (MockImeSession imeSession = MockImeSession.create( 228 InstrumentationRegistry.getInstrumentation().getContext(), 229 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 230 new ImeSettings.Builder())) { 231 final ImeEventStream stream = imeSession.openEventStream(); 232 expectEvent(stream, eventMatcher("onStartInput"), TIMEOUT); 233 234 final ImeCommand cmd = imeSession.callSwitchInputMethod(OTHER_IME_ID); 235 final ImeEvent event = expectCommand(stream, cmd, TIMEOUT); 236 assertTrue("should be exception result, but wasn't" + event, 237 event.isExceptionReturnValue()); 238 // Should be IllegalStateException, but CompletableFuture converts to RuntimeException 239 assertTrue("should be RuntimeException, but wasn't: " 240 + event.getReturnExceptionValue(), 241 event.getReturnExceptionValue() instanceof RuntimeException); 242 assertTrue( 243 "should contain 'not enabled' but didn't: " + event.getReturnExceptionValue(), 244 event.getReturnExceptionValue().getMessage().contains("not enabled")); 245 } 246 } 247 @Test 248 public void testSwitchInputMethodWithSubtype_verifiesEnabledState() throws Exception { 249 Assume.assumeFalse(isPreventImeStartup()); 250 SystemUtil.runShellCommand("ime disable " + OTHER_IME_ID); 251 try (MockImeSession imeSession = MockImeSession.create( 252 InstrumentationRegistry.getInstrumentation().getContext(), 253 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 254 new ImeSettings.Builder())) { 255 final ImeEventStream stream = imeSession.openEventStream(); 256 expectEvent(stream, eventMatcher("onStartInput"), TIMEOUT); 257 258 final ImeCommand cmd = imeSession.callSwitchInputMethod(OTHER_IME_ID, null); 259 final ImeEvent event = expectCommand(stream, cmd, TIMEOUT); 260 assertTrue("should be exception result, but wasn't" + event, 261 event.isExceptionReturnValue()); 262 // Should be IllegalStateException, but CompletableFuture converts to RuntimeException 263 assertTrue("should be RuntimeException, but wasn't: " 264 + event.getReturnExceptionValue(), 265 event.getReturnExceptionValue() instanceof RuntimeException); 266 assertTrue( 267 "should contain 'not enabled' but didn't: " + event.getReturnExceptionValue(), 268 event.getReturnExceptionValue().getMessage().contains("not enabled")); 269 } 270 } 271 272 private void verifyImeConsumesBackButton(int backDisposition) throws Exception { 273 try (MockImeSession imeSession = MockImeSession.create( 274 InstrumentationRegistry.getInstrumentation().getContext(), 275 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 276 new ImeSettings.Builder())) { 277 final ImeEventStream stream = imeSession.openEventStream(); 278 279 final TestActivity testActivity = createTestActivity(SOFT_INPUT_STATE_ALWAYS_VISIBLE); 280 expectEvent(stream, editorMatcher("onStartInputView", mMarker), TIMEOUT); 281 282 final ImeCommand command = imeSession.callSetBackDisposition(backDisposition); 283 expectCommand(stream, command, TIMEOUT); 284 285 testActivity.setIgnoreBackKey(true); 286 assertEquals(0, 287 (long) getOnMainSync(() -> testActivity.getOnBackPressedCallCount())); 288 mInstrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK); 289 290 expectEvent(stream, backKeyDownMatcher(true), CHECK_EXIT_EVENT_ONLY, TIMEOUT); 291 292 // Make sure TestActivity#onBackPressed() is NOT called. 293 try { 294 waitOnMainUntil(() -> testActivity.getOnBackPressedCallCount() > 0, 295 EXPECTED_TIMEOUT); 296 fail("Activity#onBackPressed() should not be called"); 297 } catch (TimeoutException e) { 298 // This is fine. We actually expect timeout. 299 } 300 } 301 } 302 303 @Test 304 public void testSetBackDispositionDefault() throws Exception { 305 verifyImeConsumesBackButton(InputMethodService.BACK_DISPOSITION_DEFAULT); 306 } 307 308 @Test 309 public void testSetBackDispositionWillNotDismiss() throws Exception { 310 verifyImeConsumesBackButton(InputMethodService.BACK_DISPOSITION_WILL_NOT_DISMISS); 311 } 312 313 @Test 314 public void testSetBackDispositionWillDismiss() throws Exception { 315 verifyImeConsumesBackButton(InputMethodService.BACK_DISPOSITION_WILL_DISMISS); 316 } 317 318 @Test 319 public void testSetBackDispositionAdjustNothing() throws Exception { 320 verifyImeConsumesBackButton(InputMethodService.BACK_DISPOSITION_ADJUST_NOTHING); 321 } 322 323 @Test 324 public void testRequestHideSelf() throws Exception { 325 try (MockImeSession imeSession = MockImeSession.create( 326 InstrumentationRegistry.getInstrumentation().getContext(), 327 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 328 new ImeSettings.Builder())) { 329 final ImeEventStream stream = imeSession.openEventStream(); 330 331 createTestActivity(SOFT_INPUT_STATE_ALWAYS_VISIBLE); 332 expectEvent(stream, editorMatcher("onStartInputView", mMarker), TIMEOUT); 333 334 expectImeVisible(TIMEOUT); 335 336 imeSession.callRequestHideSelf(0); 337 expectEvent(stream, eventMatcher("hideSoftInput"), TIMEOUT); 338 expectEvent(stream, eventMatcher("onFinishInputView"), TIMEOUT); 339 expectEventWithKeyValue(stream, "onWindowVisibilityChanged", "visible", 340 View.GONE, TIMEOUT); 341 342 expectImeInvisible(TIMEOUT); 343 } 344 } 345 346 @Test 347 @FlakyTest(detail = "slow test") 348 public void testRequestShowSelf() throws Exception { 349 try (MockImeSession imeSession = MockImeSession.create( 350 InstrumentationRegistry.getInstrumentation().getContext(), 351 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 352 new ImeSettings.Builder())) { 353 final ImeEventStream stream = imeSession.openEventStream(); 354 355 createTestActivity(SOFT_INPUT_STATE_ALWAYS_HIDDEN); 356 notExpectEvent( 357 stream, editorMatcher("onStartInputView", mMarker), TIMEOUT); 358 359 expectImeInvisible(TIMEOUT); 360 361 imeSession.callRequestShowSelf(0); 362 expectEvent(stream, eventMatcher("showSoftInput"), TIMEOUT); 363 expectEvent(stream, editorMatcher("onStartInputView", mMarker), TIMEOUT); 364 expectEventWithKeyValue(stream, "onWindowVisibilityChanged", "visible", 365 View.VISIBLE, TIMEOUT); 366 367 expectImeVisible(TIMEOUT); 368 } 369 } 370 371 @FlakyTest(bugId = 210680326) 372 @Test 373 public void testHandlesConfigChanges() throws Exception { 374 try (MockImeSession imeSession = MockImeSession.create( 375 InstrumentationRegistry.getInstrumentation().getContext(), 376 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 377 new ImeSettings.Builder())) { 378 final ImeEventStream stream = imeSession.openEventStream(); 379 380 // Case 1: Activity handles configChanges="fontScale" 381 createTestActivity(SOFT_INPUT_STATE_ALWAYS_VISIBLE); 382 expectEvent(stream, editorMatcher("onStartInput", mMarker), TIMEOUT); 383 expectEvent(stream, eventMatcher("showSoftInput"), TIMEOUT); 384 // MockIme handles fontScale. Make sure changing fontScale doesn't restart IME. 385 enableFontScale(); 386 expectImeVisible(TIMEOUT); 387 // Make sure IME was not restarted. 388 notExpectEvent(stream, eventMatcher("onCreate"), 389 EXPECTED_TIMEOUT); 390 notExpectEvent(stream, showSoftInputMatcher(0), 391 EXPECTED_TIMEOUT); 392 393 eraseFontScale(); 394 395 // Case 2: Activity *doesn't* handle configChanges="fontScale" and restarts. 396 createTestActivity2(SOFT_INPUT_STATE_ALWAYS_VISIBLE); 397 expectEvent(stream, editorMatcher("onStartInput", mMarker), TIMEOUT); 398 // MockIme handles fontScale. Make sure changing fontScale doesn't restart IME. 399 enableFontScale(); 400 expectImeVisible(TIMEOUT); 401 // Make sure IME was not restarted. 402 notExpectEvent(stream, eventMatcher("onCreate"), 403 EXPECTED_TIMEOUT); 404 } finally { 405 eraseFontScale(); 406 } 407 } 408 409 /** 410 * Font scale is a global configuration. 411 * This function will apply font scale changes. 412 */ 413 private void enableFontScale() { 414 try { 415 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 416 SystemUtil.runShellCommand(instrumentation, PUT_FONT_SCALE_CMD); 417 instrumentation.waitForIdleSync(); 418 } catch (IOException io) { 419 fail("Couldn't apply font scale."); 420 } 421 } 422 423 /** 424 * Font scale is a global configuration. 425 * This function will apply font scale changes. 426 */ 427 private void eraseFontScale() { 428 try { 429 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 430 SystemUtil.runShellCommand(instrumentation, ERASE_FONT_SCALE_CMD); 431 instrumentation.waitForIdleSync(); 432 } catch (IOException io) { 433 fail("Couldn't apply font scale."); 434 } 435 } 436 437 private static void assertSynthesizedSoftwareKeyEvent(KeyEvent keyEvent, int expectedAction, 438 int expectedKeyCode, long expectedEventTimeBefore, long expectedEventTimeAfter) { 439 if (keyEvent.getEventTime() < expectedEventTimeBefore 440 || expectedEventTimeAfter < keyEvent.getEventTime()) { 441 fail(String.format("EventTime must be within [%d, %d]," 442 + " which was %d", expectedEventTimeBefore, expectedEventTimeAfter, 443 keyEvent.getEventTime())); 444 } 445 assertEquals(expectedAction, keyEvent.getAction()); 446 assertEquals(expectedKeyCode, keyEvent.getKeyCode()); 447 assertEquals(KeyCharacterMap.VIRTUAL_KEYBOARD, keyEvent.getDeviceId()); 448 assertEquals(0, keyEvent.getScanCode()); 449 assertEquals(0, keyEvent.getRepeatCount()); 450 assertEquals(0, keyEvent.getRepeatCount()); 451 final int mustHaveFlags = KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE; 452 final int mustNotHaveFlags = KeyEvent.FLAG_FROM_SYSTEM; 453 if ((keyEvent.getFlags() & mustHaveFlags) == 0 454 || (keyEvent.getFlags() & mustNotHaveFlags) != 0) { 455 fail(String.format("Flags must have FLAG_SOFT_KEYBOARD|" 456 + "FLAG_KEEP_TOUCH_MODE and must not have FLAG_FROM_SYSTEM, " 457 + "which was 0x%08X", keyEvent.getFlags())); 458 } 459 } 460 461 /** 462 * Test compatibility requirements of {@link InputMethodService#sendDownUpKeyEvents(int)}. 463 */ 464 @Test 465 public void testSendDownUpKeyEvents() throws Exception { 466 try (MockImeSession imeSession = MockImeSession.create( 467 InstrumentationRegistry.getInstrumentation().getContext(), 468 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 469 new ImeSettings.Builder())) { 470 final ImeEventStream stream = imeSession.openEventStream(); 471 472 final AtomicReference<ArrayList<KeyEvent>> keyEventsRef = new AtomicReference<>(); 473 final String marker = "testSendDownUpKeyEvents/" + SystemClock.elapsedRealtimeNanos(); 474 475 TestActivity.startSync(activity -> { 476 final LinearLayout layout = new LinearLayout(activity); 477 layout.setOrientation(LinearLayout.VERTICAL); 478 479 final ArrayList<KeyEvent> keyEvents = new ArrayList<>(); 480 keyEventsRef.set(keyEvents); 481 final EditText editText = new EditText(activity) { 482 @Override 483 public InputConnection onCreateInputConnection(EditorInfo editorInfo) { 484 return new InputConnectionWrapper( 485 super.onCreateInputConnection(editorInfo), false) { 486 /** 487 * {@inheritDoc} 488 */ 489 @Override 490 public boolean sendKeyEvent(KeyEvent event) { 491 keyEvents.add(event); 492 return super.sendKeyEvent(event); 493 } 494 }; 495 } 496 }; 497 editText.setPrivateImeOptions(marker); 498 layout.addView(editText); 499 editText.requestFocus(); 500 return layout; 501 }); 502 503 // Wait until "onStartInput" gets called for the EditText. 504 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 505 506 // Make sure that InputConnection#sendKeyEvent() has never been called yet. 507 assertTrue(TestUtils.getOnMainSync( 508 () -> new ArrayList<>(keyEventsRef.get())).isEmpty()); 509 510 final int expectedKeyCode = KeyEvent.KEYCODE_0; 511 final long uptimeStart = SystemClock.uptimeMillis(); 512 expectCommand(stream, imeSession.callSendDownUpKeyEvents(expectedKeyCode), TIMEOUT); 513 final long uptimeEnd = SystemClock.uptimeMillis(); 514 515 final ArrayList<KeyEvent> keyEvents = TestUtils.getOnMainSync( 516 () -> new ArrayList<>(keyEventsRef.get())); 517 518 // Check KeyEvent objects. 519 assertNotNull(keyEvents); 520 assertEquals(2, keyEvents.size()); 521 assertSynthesizedSoftwareKeyEvent(keyEvents.get(0), KeyEvent.ACTION_DOWN, 522 expectedKeyCode, uptimeStart, uptimeEnd); 523 assertSynthesizedSoftwareKeyEvent(keyEvents.get(1), KeyEvent.ACTION_UP, 524 expectedKeyCode, uptimeStart, uptimeEnd); 525 final Bundle arguments = expectEvent(stream, 526 eventMatcher("onUpdateSelection"), 527 TIMEOUT).getArguments(); 528 expectOnUpdateSelectionArguments(arguments, 0, 0, 1, 1, -1, -1); 529 } 530 } 531 532 /** 533 * Ensure that {@link InputConnection#requestCursorUpdates(int)} works for the built-in 534 * {@link EditText} and {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} 535 * will be called back. 536 */ 537 @Test 538 public void testOnUpdateCursorAnchorInfo() throws Exception { 539 try (MockImeSession imeSession = MockImeSession.create( 540 InstrumentationRegistry.getInstrumentation().getContext(), 541 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 542 new ImeSettings.Builder())) { 543 final String marker = 544 "testOnUpdateCursorAnchorInfo()/" + SystemClock.elapsedRealtimeNanos(); 545 546 final AtomicReference<EditText> editTextRef = new AtomicReference<>(); 547 final AtomicInteger requestCursorUpdatesCallCount = new AtomicInteger(); 548 final AtomicInteger requestCursorUpdatesWithFilterCallCount = new AtomicInteger(); 549 TestActivity.startSync(activity -> { 550 final LinearLayout layout = new LinearLayout(activity); 551 layout.setOrientation(LinearLayout.VERTICAL); 552 553 final EditText editText = new EditText(activity) { 554 @Override 555 public InputConnection onCreateInputConnection(EditorInfo outAttrs) { 556 final InputConnection original = super.onCreateInputConnection(outAttrs); 557 return new InputConnectionWrapper(original, false) { 558 @Override 559 public boolean requestCursorUpdates(int cursorUpdateMode) { 560 if ((cursorUpdateMode & InputConnection.CURSOR_UPDATE_IMMEDIATE) 561 != 0) { 562 requestCursorUpdatesCallCount.incrementAndGet(); 563 return true; 564 } 565 return false; 566 } 567 568 @Override 569 public boolean requestCursorUpdates( 570 int cursorUpdateMode, int cursorUpdateFilter) { 571 requestCursorUpdatesWithFilterCallCount.incrementAndGet(); 572 return requestCursorUpdates(cursorUpdateMode | cursorUpdateFilter); 573 } 574 }; 575 } 576 }; 577 editTextRef.set(editText); 578 editText.setPrivateImeOptions(marker); 579 layout.addView(editText); 580 editText.requestFocus(); 581 return layout; 582 }); 583 final EditText editText = editTextRef.get(); 584 585 final ImeEventStream stream = imeSession.openEventStream(); 586 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 587 588 // Make sure that InputConnection#requestCursorUpdates() returns true. 589 assertTrue(expectCommand(stream, 590 imeSession.callRequestCursorUpdates(InputConnection.CURSOR_UPDATE_IMMEDIATE), 591 TIMEOUT).getReturnBooleanValue()); 592 593 // Also make sure that requestCursorUpdates() actually gets called only once. 594 assertEquals(1, requestCursorUpdatesCallCount.get()); 595 596 final CursorAnchorInfo originalCursorAnchorInfo = new CursorAnchorInfo.Builder() 597 .setMatrix(new Matrix()) 598 .setInsertionMarkerLocation(3.0f, 4.0f, 5.0f, 6.0f, 0) 599 .setSelectionRange(7, 8) 600 .build(); 601 602 runOnMainSync(() -> editText.getContext().getSystemService(InputMethodManager.class) 603 .updateCursorAnchorInfo(editText, originalCursorAnchorInfo)); 604 605 final CursorAnchorInfo receivedCursorAnchorInfo = expectEvent(stream, 606 eventMatcher("onUpdateCursorAnchorInfo"), 607 TIMEOUT).getArguments().getParcelable("cursorAnchorInfo"); 608 assertNotNull(receivedCursorAnchorInfo); 609 assertEquals(receivedCursorAnchorInfo, originalCursorAnchorInfo); 610 611 requestCursorUpdatesCallCount.set(0); 612 // Request Cursor updates with Filter 613 // Make sure that InputConnection#requestCursorUpdates() returns true with data filter. 614 assertTrue(expectCommand(stream, 615 imeSession.callRequestCursorUpdates( 616 InputConnection.CURSOR_UPDATE_IMMEDIATE 617 | InputConnection.CURSOR_UPDATE_FILTER_EDITOR_BOUNDS 618 | InputConnection.CURSOR_UPDATE_FILTER_CHARACTER_BOUNDS 619 | InputConnection.CURSOR_UPDATE_FILTER_INSERTION_MARKER 620 | InputConnection.CURSOR_UPDATE_FILTER_VISIBLE_LINE_BOUNDS 621 | InputConnection.CURSOR_UPDATE_FILTER_TEXT_APPEARANCE), 622 TIMEOUT).getReturnBooleanValue()); 623 624 // Also make sure that requestCursorUpdates() actually gets called only once. 625 assertEquals(1, requestCursorUpdatesCallCount.get()); 626 627 EditorBoundsInfo.Builder builder = new EditorBoundsInfo.Builder(); 628 builder.setEditorBounds(new RectF(0f, 1f, 2f, 3f)); 629 final CursorAnchorInfo originalCursorAnchorInfo1 = new CursorAnchorInfo.Builder() 630 .setMatrix(new Matrix()) 631 .setEditorBoundsInfo(builder.build()) 632 .addVisibleLineBounds(1f, 2f, 3f, 5f) 633 .setTextAppearanceInfo(new TextAppearanceInfo.Builder().build()) 634 .build(); 635 636 runOnMainSync(() -> editText.getContext().getSystemService(InputMethodManager.class) 637 .updateCursorAnchorInfo(editText, originalCursorAnchorInfo1)); 638 639 final CursorAnchorInfo receivedCursorAnchorInfo1 = expectEvent(stream, 640 eventMatcher("onUpdateCursorAnchorInfo"), 641 TIMEOUT).getArguments().getParcelable("cursorAnchorInfo"); 642 assertNotNull(receivedCursorAnchorInfo1); 643 assertEquals(receivedCursorAnchorInfo1, originalCursorAnchorInfo1); 644 645 requestCursorUpdatesCallCount.set(0); 646 requestCursorUpdatesWithFilterCallCount.set(0); 647 // Request Cursor updates with Mode and Filter 648 // Make sure that InputConnection#requestCursorUpdates() returns true with mode and 649 // data filter. 650 builder = new EditorBoundsInfo.Builder(); 651 builder.setEditorBounds(new RectF(1f, 1f, 2f, 3f)); 652 final CursorAnchorInfo originalCursorAnchorInfo2 = new CursorAnchorInfo.Builder() 653 .setMatrix(new Matrix()) 654 .setEditorBoundsInfo(builder.build()) 655 .addVisibleLineBounds(1f, 2f, 3f, 4f) 656 .setTextAppearanceInfo(new TextAppearanceInfo.Builder().build()) 657 .build(); 658 assertTrue(expectCommand(stream, 659 imeSession.callRequestCursorUpdates( 660 InputConnection.CURSOR_UPDATE_IMMEDIATE, 661 InputConnection.CURSOR_UPDATE_FILTER_EDITOR_BOUNDS 662 | InputConnection.CURSOR_UPDATE_FILTER_CHARACTER_BOUNDS 663 | InputConnection.CURSOR_UPDATE_FILTER_INSERTION_MARKER 664 | InputConnection.CURSOR_UPDATE_FILTER_VISIBLE_LINE_BOUNDS 665 | InputConnection.CURSOR_UPDATE_FILTER_TEXT_APPEARANCE), 666 TIMEOUT).getReturnBooleanValue()); 667 668 // Make sure that requestCursorUpdates() actually gets called only once. 669 assertEquals(1, requestCursorUpdatesCallCount.get()); 670 assertEquals(1, requestCursorUpdatesWithFilterCallCount.get()); 671 runOnMainSync(() -> editText.getContext().getSystemService(InputMethodManager.class) 672 .updateCursorAnchorInfo(editText, originalCursorAnchorInfo2)); 673 674 final CursorAnchorInfo receivedCursorAnchorInfo2 = expectEvent(stream, 675 eventMatcher("onUpdateCursorAnchorInfo"), 676 TIMEOUT).getArguments().getParcelable("cursorAnchorInfo"); 677 assertNotNull(receivedCursorAnchorInfo2); 678 assertEquals(receivedCursorAnchorInfo2, originalCursorAnchorInfo2); 679 } 680 } 681 682 /** Test that no exception is thrown when {@link InputMethodService#getDisplay()} is called */ 683 @Test 684 public void testGetDisplay() throws Exception { 685 try (MockImeSession imeSession = MockImeSession.create( 686 mInstrumentation.getContext(), mInstrumentation.getUiAutomation(), 687 new ImeSettings.Builder().setVerifyUiContextApisInOnCreate(true))) { 688 ensureImeRunning(); 689 final ImeEventStream stream = imeSession.openEventStream(); 690 691 // Verify if getDisplay doesn't throw exception before InputMethodService's 692 // initialization. 693 assertTrue(expectEvent(stream, verificationMatcher("getDisplay"), 694 CHECK_EXIT_EVENT_ONLY, TIMEOUT).getReturnBooleanValue()); 695 createTestActivity(SOFT_INPUT_STATE_ALWAYS_VISIBLE); 696 697 expectEvent(stream, editorMatcher("onStartInput", mMarker), TIMEOUT); 698 // Verify if getDisplay doesn't throw exception 699 assertTrue(expectCommand(stream, imeSession.callVerifyGetDisplay(), TIMEOUT) 700 .getReturnBooleanValue()); 701 } 702 } 703 704 /** Test the cursor position of {@link EditText} is correct after typing on another activity. */ 705 @Test 706 public void testCursorAfterLaunchAnotherActivity() throws Exception { 707 final AtomicReference<EditText> firstEditTextRef = new AtomicReference<>(); 708 final int newCursorOffset = 5; 709 final String initialText = "Initial"; 710 final String firstCommitMsg = "First"; 711 final String secondCommitMsg = "Second"; 712 713 try (MockImeSession imeSession = MockImeSession.create( 714 InstrumentationRegistry.getInstrumentation().getContext(), 715 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 716 new ImeSettings.Builder())) { 717 final String marker = 718 "testCursorAfterLaunchAnotherActivity()/" + SystemClock.elapsedRealtimeNanos(); 719 720 // Launch first test activity 721 TestActivity.startSync(activity -> { 722 final LinearLayout layout = new LinearLayout(activity); 723 layout.setOrientation(LinearLayout.VERTICAL); 724 725 final EditText editText = new EditText(activity); 726 editText.setPrivateImeOptions(marker); 727 editText.setSingleLine(false); 728 firstEditTextRef.set(editText); 729 editText.setText(initialText); 730 layout.addView(editText); 731 editText.requestFocus(); 732 return layout; 733 }); 734 735 final EditText firstEditText = firstEditTextRef.get(); 736 final ImeEventStream stream = imeSession.openEventStream(); 737 738 // Verify onStartInput when first activity launch 739 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 740 741 final ImeCommand commit = imeSession.callCommitText(firstCommitMsg, 1); 742 expectCommand(stream, commit, TIMEOUT); 743 TestUtils.waitOnMainUntil( 744 () -> TextUtils.equals( 745 firstEditText.getText(), initialText + firstCommitMsg), TIMEOUT); 746 747 // Get current position 748 int originalSelectionStart = firstEditText.getSelectionStart(); 749 int originalSelectionEnd = firstEditText.getSelectionEnd(); 750 751 assertEquals(initialText.length() + firstCommitMsg.length(), originalSelectionStart); 752 assertEquals(initialText.length() + firstCommitMsg.length(), originalSelectionEnd); 753 754 // Launch second test activity 755 final Intent intent = new Intent() 756 .setAction(Intent.ACTION_MAIN) 757 .setClass(InstrumentationRegistry.getInstrumentation().getContext(), 758 TestActivity.class) 759 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 760 TestActivity secondActivity = (TestActivity) InstrumentationRegistry 761 .getInstrumentation().startActivitySync(intent); 762 763 // Verify onStartInput when second activity launch 764 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 765 766 // Commit some messages on second activity 767 final ImeCommand secondCommit = imeSession.callCommitText(secondCommitMsg, 1); 768 expectCommand(stream, secondCommit, TIMEOUT); 769 770 // Back to first activity 771 runOnMainSync(secondActivity::onBackPressed); 772 773 // Make sure TestActivity#onBackPressed() is called. 774 TestUtils.waitOnMainUntil(() -> secondActivity.getOnBackPressedCallCount() > 0, 775 TIMEOUT, "Activity#onBackPressed() should be called"); 776 777 TestUtils.runOnMainSync(firstEditText::requestFocus); 778 779 // Verify onStartInput when first activity launch 780 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 781 782 // Update cursor to a new position 783 int newCursorPosition = originalSelectionStart - newCursorOffset; 784 final ImeCommand setSelection = 785 imeSession.callSetSelection(newCursorPosition, newCursorPosition); 786 expectCommand(stream, setSelection, TIMEOUT); 787 788 // Commit to first activity again 789 final ImeCommand commitFirstAgain = imeSession.callCommitText(firstCommitMsg, 1); 790 expectCommand(stream, commitFirstAgain, TIMEOUT); 791 TestUtils.waitOnMainUntil( 792 () -> TextUtils.equals(firstEditText.getText(), "InitialFirstFirst"), TIMEOUT); 793 794 // get new position 795 int newSelectionStart = firstEditText.getSelectionStart(); 796 int newSelectionEnd = firstEditText.getSelectionEnd(); 797 798 assertEquals(newSelectionStart, newCursorPosition + firstCommitMsg.length()); 799 assertEquals(newSelectionEnd, newCursorPosition + firstCommitMsg.length()); 800 } 801 } 802 803 @Test 804 public void testBatchEdit_commitAndSetComposingRegion_textView() throws Exception { 805 getCommitAndSetComposingRegionTest(TIMEOUT, 806 "testBatchEdit_commitAndSetComposingRegion_textView/") 807 .setTestTextView(true) 808 .runTest(); 809 } 810 811 @FlakyTest(bugId = 300314534) 812 @Test 813 public void testBatchEdit_commitAndSetComposingRegion_webView() throws Exception { 814 assumeTrue(hasFeatureWebView()); 815 816 getCommitAndSetComposingRegionTest(TIMEOUT, 817 "testBatchEdit_commitAndSetComposingRegion_webView/") 818 .setTestTextView(false) 819 .runTest(); 820 } 821 822 @Test 823 public void testBatchEdit_commitSpaceThenSetComposingRegion_textView() throws Exception { 824 getCommitSpaceAndSetComposingRegionTest(TIMEOUT, 825 "testBatchEdit_commitSpaceThenSetComposingRegion_textView/") 826 .setTestTextView(true) 827 .runTest(); 828 } 829 830 @Test 831 @FlakyTest(bugId = 294840051) 832 public void testBatchEdit_commitSpaceThenSetComposingRegion_webView() throws Exception { 833 assumeTrue(hasFeatureWebView()); 834 835 getCommitSpaceAndSetComposingRegionTest(TIMEOUT, 836 "testBatchEdit_commitSpaceThenSetComposingRegion_webView/") 837 .setTestTextView(false) 838 .runTest(); 839 } 840 841 @Test 842 public void testBatchEdit_getCommitSpaceAndSetComposingRegionTestInSelectionTest_textView() 843 throws Exception { 844 getCommitSpaceAndSetComposingRegionInSelectionTest(TIMEOUT, 845 "testBatchEdit_getCommitSpaceAndSetComposingRegionTestInSelectionTest_textView/") 846 .setTestTextView(true) 847 .runTest(); 848 } 849 850 @FlakyTest(bugId = 333155542) 851 @Test 852 public void testBatchEdit_getCommitSpaceAndSetComposingRegionTestInSelectionTest_webView() 853 throws Exception { 854 assumeTrue(hasFeatureWebView()); 855 856 getCommitSpaceAndSetComposingRegionInSelectionTest(TIMEOUT, 857 "testBatchEdit_getCommitSpaceAndSetComposingRegionTestInSelectionTest_webView/") 858 .setTestTextView(false) 859 .runTest(); 860 } 861 862 private boolean hasFeatureWebView() { 863 final PackageManager pm = 864 InstrumentationRegistry.getInstrumentation().getContext().getPackageManager(); 865 return pm.hasSystemFeature(PackageManager.FEATURE_WEBVIEW); 866 } 867 868 @Test 869 public void testImeVisibleAfterRotation() throws Exception { 870 try (MockImeSession imeSession = MockImeSession.create( 871 InstrumentationRegistry.getInstrumentation().getContext(), 872 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 873 new ImeSettings.Builder())) { 874 final ImeEventStream stream = imeSession.openEventStream(); 875 876 final Activity activity = createTestActivity(SOFT_INPUT_STATE_ALWAYS_VISIBLE); 877 expectEvent(stream, editorMatcher("onStartInput", mMarker), TIMEOUT); 878 final int initialOrientation = activity.getRequestedOrientation(); 879 try { 880 activity.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE); 881 mInstrumentation.waitForIdleSync(); 882 expectImeVisible(TIMEOUT); 883 884 activity.setRequestedOrientation(SCREEN_ORIENTATION_PORTRAIT); 885 mInstrumentation.waitForIdleSync(); 886 expectImeVisible(TIMEOUT); 887 } finally { 888 if (initialOrientation != SCREEN_ORIENTATION_PORTRAIT) { 889 activity.setRequestedOrientation(initialOrientation); 890 } 891 } 892 } 893 } 894 895 /** 896 * Starts a {@link MockImeSession} and verifies MockIme receives {@link WindowLayoutInfo} 897 * updates. Trigger Configuration changes by modifying the DisplaySession where MockIME window 898 * is located, then verify Bounds from MockIME window and {@link DisplayFeature} from 899 * WindowLayoutInfo updates observe the same changes to the hinge location. 900 * Here we use {@link WindowLayoutInfoParcelable} to pass {@link WindowLayoutInfo} values 901 * between this test process and the MockIME process. 902 */ 903 @Test 904 @ApiTest(apis = { 905 "androidx.window.extensions.layout.WindowLayoutComponent#addWindowLayoutInfoListener"}) 906 public void testImeListensToWindowLayoutInfo() throws Exception { 907 assumeExtensionSupportedDevice(); 908 909 final double resizeRatio = 0.8; 910 try (MockImeSession imeSession = MockImeSession.create( 911 InstrumentationRegistry.getInstrumentation().getContext(), 912 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 913 new ImeSettings.Builder().setWindowLayoutInfoCallbackEnabled(true))) { 914 915 final ImeEventStream stream = imeSession.openEventStream(); 916 TestActivity activity = createTestActivity(SOFT_INPUT_STATE_ALWAYS_VISIBLE); 917 assertThat(expectEvent(stream, verificationMatcher("windowLayoutComponentLoaded"), 918 CHECK_EXIT_EVENT_ONLY, TIMEOUT).getReturnBooleanValue()).isTrue(); 919 final Display display = activity.getDisplay(); 920 assertThat(display).isNotNull(); 921 922 final int displayId = display.getDisplayId(); 923 try (DisplayMetricsSession displaySession = new DisplayMetricsSession(displayId)) { 924 // MockIME has registered addWindowLayoutInfo, it should be emitting the 925 // current location of hinge now. 926 WindowLayoutInfoParcelable windowLayoutInit = verifyReceivedWindowLayout(stream); 927 assertThat(windowLayoutInit).isNotNull(); 928 final List<DisplayFeature> featuresInit = windowLayoutInit.getDisplayFeatures(); 929 assertThat(featuresInit).isNotNull(); 930 931 // Skip the test if the device doesn't support hinges. 932 assume().that(featuresInit).isNotEmpty(); 933 934 final Rect windowBoundsInit = featuresInit.get(0).getBounds(); 935 expectEvent(stream, editorMatcher("onStartInput", mMarker), TIMEOUT); 936 expectEvent(stream, eventMatcher("showSoftInput"), TIMEOUT); 937 938 // After IME is shown, get the bounds of IME. 939 final Rect imeBoundsInit = expectCommand(stream, 940 imeSession.callGetCurrentWindowMetricsBounds(), TIMEOUT) 941 .getReturnParcelableValue(); 942 943 // Contain first part of the test in a try-block so that the display session 944 // could be restored for the remaining testsuite even if something fails. 945 try { 946 // Shrink the entire display 20% smaller. 947 displaySession.changeDisplayMetrics(resizeRatio /* sizeRatio */, 948 1.0 /* densityRatio */); 949 950 // onConfigurationChanged on WM side triggers a new calculation for 951 // hinge location. 952 final WindowLayoutInfoParcelable windowLayoutResized = 953 verifyReceivedWindowLayout(stream); 954 955 // Expect to receive same number of display features in WindowLayoutInfo. 956 final List<DisplayFeature> featuresResized = 957 windowLayoutResized.getDisplayFeatures(); 958 assertThat(featuresResized).hasSize(featuresInit.size()); 959 960 final Rect windowBoundsResized = featuresResized.get(0).getBounds(); 961 final Rect imeBoundsResized = expectCommand(stream, 962 imeSession.callGetCurrentWindowMetricsBounds(), TIMEOUT) 963 .getReturnParcelableValue(); 964 965 final StringBuilder errorMessage = new StringBuilder(); 966 final Function<Function<Rect, Integer>, Boolean> inSameRatio = getSize -> { 967 final boolean windowResizedCorrectly = isResizedWithRatio(getSize, 968 windowBoundsInit, windowBoundsResized, resizeRatio, errorMessage); 969 final boolean imeResizedCorrectly = isResizedWithRatio(getSize, 970 imeBoundsInit, imeBoundsResized, resizeRatio, errorMessage); 971 return windowResizedCorrectly && imeResizedCorrectly; 972 }; 973 final boolean widthsChangedInSameRatio = inSameRatio.apply(Rect::width); 974 final boolean heightsChangedInSameRatio = inSameRatio.apply(Rect::height); 975 976 // Expect the hinge dimension to shrink in exactly one direction, the actual 977 // dimension depends on device implementation. Observe hinge dimensions from 978 // IME configuration bounds and from WindowLayoutInfo. 979 assertWithMessage( 980 "Expected either width or height to change proportionally.\n" 981 + " widthsChangedInSameRatio: " 982 + widthsChangedInSameRatio + "\n" 983 + " heightsChangedInSameRatio: " 984 + heightsChangedInSameRatio + "\n" 985 + " Resize ratio: " + String.format("%.1f", resizeRatio) + "\n" 986 + " Initial window bounds: " + windowBoundsInit + "\n" 987 + " Resized window bounds: " + windowBoundsResized + "\n" 988 + " Initial IME bounds: " + imeBoundsInit + "\n" 989 + " Resized IME bounds: " + imeBoundsResized + "\n" 990 + " Details:" + errorMessage) 991 .that(widthsChangedInSameRatio || heightsChangedInSameRatio) 992 .isTrue(); 993 } finally { 994 // Restore Display to original size. 995 displaySession.restoreDisplayMetrics(); 996 } 997 998 final WindowLayoutInfoParcelable restored = verifyReceivedWindowLayout(stream); 999 final List<DisplayFeature> features = restored.getDisplayFeatures(); 1000 assertThat(features).isNotEmpty(); 1001 assertThat(features.get(0).getBounds()).isEqualTo(windowBoundsInit); 1002 1003 final Rect imeBoundsRestored = expectCommand(stream, 1004 imeSession.callGetCurrentWindowMetricsBounds(), TIMEOUT) 1005 .getReturnParcelableValue(); 1006 assertThat(imeBoundsRestored).isEqualTo(imeBoundsInit); 1007 } 1008 } 1009 } 1010 1011 /** Verify if {@link InputMethodService#isUiContext()} returns {@code true}. */ 1012 @Test 1013 public void testIsUiContext() throws Exception { 1014 try (MockImeSession imeSession = MockImeSession.create( 1015 mInstrumentation.getContext(), mInstrumentation.getUiAutomation(), 1016 new ImeSettings.Builder().setVerifyUiContextApisInOnCreate(true))) { 1017 ensureImeRunning(); 1018 final ImeEventStream stream = imeSession.openEventStream(); 1019 1020 // Verify if InputMethodService#isUiContext returns true in #onCreate 1021 assertTrue(expectEvent(stream, verificationMatcher("isUiContext"), 1022 CHECK_EXIT_EVENT_ONLY, TIMEOUT).getReturnBooleanValue()); 1023 createTestActivity(SOFT_INPUT_STATE_ALWAYS_VISIBLE); 1024 1025 expectEvent(stream, editorMatcher("onStartInput", mMarker), TIMEOUT); 1026 // Verify if InputMethodService#isUiContext returns true 1027 assertTrue(expectCommand(stream, imeSession.callVerifyIsUiContext(), TIMEOUT) 1028 .getReturnBooleanValue()); 1029 } 1030 } 1031 1032 @Test 1033 public void testNoConfigurationChangedOnStartInput() throws Exception { 1034 try (MockImeSession imeSession = MockImeSession.create( 1035 mInstrumentation.getContext(), mInstrumentation.getUiAutomation(), 1036 new ImeSettings.Builder())) { 1037 final ImeEventStream stream = imeSession.openEventStream(); 1038 1039 createTestActivity(SOFT_INPUT_STATE_ALWAYS_VISIBLE); 1040 1041 final ImeEventStream forkedStream = stream.copy(); 1042 expectEvent(stream, editorMatcher("onStartInput", mMarker), TIMEOUT); 1043 // Verify if InputMethodService#isUiContext returns true 1044 notExpectEvent(forkedStream, eventMatcher("onConfigurationChanged"), EXPECTED_TIMEOUT); 1045 } 1046 } 1047 1048 @Test 1049 public void testShowSoftInput_whenAllImesDisabled() { 1050 final InputMethodManager inputManager = 1051 mInstrumentation.getTargetContext().getSystemService(InputMethodManager.class); 1052 assertNotNull(inputManager); 1053 final List<InputMethodInfo> enabledImes = inputManager.getEnabledInputMethodList(); 1054 1055 try { 1056 // disable all IMEs 1057 for (InputMethodInfo ime : enabledImes) { 1058 SystemUtil.runShellCommand("ime disable " + ime.getId()); 1059 } 1060 1061 // start a test activity and expect it not to crash 1062 createTestActivity(SOFT_INPUT_STATE_ALWAYS_VISIBLE); 1063 } finally { 1064 // restore all previous IMEs 1065 SystemUtil.runShellCommand("ime reset"); 1066 } 1067 } 1068 1069 @Test 1070 public void testImeOverrideSessionInterface_throwLinkageError() { 1071 SystemUtil.runCommandAndPrintOnLogcat(TAG, "am compat enable " 1072 + DISALLOW_INPUT_METHOD_INTERFACE_OVERRIDE + " " + DISAPPROVE_IME_PACKAGE_NAME); 1073 1074 final Context context = mInstrumentation.getContext(); 1075 final CountDownLatch serviceCreateLatch = new CountDownLatch(1); 1076 final DisapproveInputMethodService.DisapproveImeCallback disapproveImeCallback = 1077 hasLinkageError -> { 1078 if (!hasLinkageError) { 1079 fail("Should throw a LinkageError."); 1080 } 1081 serviceCreateLatch.countDown(); 1082 }; 1083 1084 try { 1085 DisapproveInputMethodService.setCallback(disapproveImeCallback); 1086 mServiceRule.bindService(new Intent(context, DisapproveInputMethodService.class)); 1087 } catch (TimeoutException e) { 1088 fail("DisapproveInputMethodService binding timeout."); 1089 } 1090 1091 boolean result = false; 1092 try { 1093 result = serviceCreateLatch.await(EXPECTED_TIMEOUT, TimeUnit.MILLISECONDS); 1094 if (!result) { 1095 fail("Timeout before receiving the result."); 1096 } 1097 } catch (InterruptedException ignored) { 1098 } finally { 1099 SystemUtil.runCommandAndPrintOnLogcat(TAG, "am compat reset " 1100 + DISALLOW_INPUT_METHOD_INTERFACE_OVERRIDE + " " + DISAPPROVE_IME_PACKAGE_NAME); 1101 } 1102 } 1103 1104 /** 1105 * Verifies that requesting to hide the IME caption bar does not lead 1106 * to any undesired behaviour (e.g. crashing, hiding the IME when it was visible, etc.). 1107 */ 1108 @Test 1109 public void testRequestHideImeCaptionBar() throws Exception { 1110 try (MockImeSession imeSession = MockImeSession.create( 1111 InstrumentationRegistry.getInstrumentation().getContext(), 1112 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 1113 new ImeSettings.Builder())) { 1114 final ImeEventStream stream = imeSession.openEventStream(); 1115 1116 createTestActivity(SOFT_INPUT_STATE_ALWAYS_VISIBLE); 1117 expectEvent(stream, editorMatcher("onStartInput", mMarker), TIMEOUT); 1118 expectImeVisible(TIMEOUT); 1119 1120 expectCommand(stream, imeSession.callSetImeCaptionBarVisible(false), TIMEOUT); 1121 expectImeVisible(TIMEOUT); 1122 } 1123 } 1124 1125 /** 1126 * Verifies that requesting to hide the IME caption bar and then show it again does not lead 1127 * to any undesired behaviour (e.g. crashing, hiding the IME when it was visible, etc.). 1128 */ 1129 @Test 1130 public void testRequestHideThenShowImeCaptionBar() throws Exception { 1131 try (MockImeSession imeSession = MockImeSession.create( 1132 InstrumentationRegistry.getInstrumentation().getContext(), 1133 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 1134 new ImeSettings.Builder())) { 1135 final ImeEventStream stream = imeSession.openEventStream(); 1136 1137 createTestActivity(SOFT_INPUT_STATE_ALWAYS_VISIBLE); 1138 expectEvent(stream, editorMatcher("onStartInput", mMarker), TIMEOUT); 1139 expectImeVisible(TIMEOUT); 1140 1141 expectCommand(stream, imeSession.callSetImeCaptionBarVisible(false), TIMEOUT); 1142 expectImeVisible(TIMEOUT); 1143 1144 expectCommand(stream, imeSession.callSetImeCaptionBarVisible(true), TIMEOUT); 1145 expectImeVisible(TIMEOUT); 1146 } 1147 } 1148 1149 /** 1150 * Checks that the IME insets are at least as big as the IME navigation bar (when visible), 1151 * even if the IME overrides the insets, or gives an empty input view. 1152 */ 1153 @Test 1154 public void testImeNavigationBarInsets() throws Exception { 1155 runImeNavigationBarTest(false /* useFullscreenMode */); 1156 } 1157 1158 /** 1159 * Checks that the IME insets are not modified to be at least as big as the IME navigation bar, 1160 * when the IME is using fullscreen mode. 1161 */ 1162 @Test 1163 public void testImeNavigationBarInsets_FullscreenMode() throws Exception { 1164 runImeNavigationBarTest(true /* useFullscreenMode */); 1165 } 1166 1167 /** 1168 * Test implementation for checking that the IME insets are at least as big as the IME 1169 * navigation bar (when visible). When using fullscreen mode, the IME requesting app should 1170 * receive zero IME insets. 1171 * 1172 * @param useFullscreenMode whether the IME should use the fullscreen mode. 1173 */ 1174 private void runImeNavigationBarTest(boolean useFullscreenMode) throws Exception { 1175 try (MockImeSession imeSession = MockImeSession.create( 1176 InstrumentationRegistry.getInstrumentation().getContext(), 1177 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 1178 new ImeSettings.Builder() 1179 .setZeroInsets(true) 1180 .setDrawsBehindNavBar(true) 1181 .setFullscreenModePolicy( 1182 useFullscreenMode 1183 ? ImeSettings.FullscreenModePolicy.FORCE_FULLSCREEN 1184 : ImeSettings.FullscreenModePolicy.NO_FULLSCREEN))) { 1185 final ImeEventStream stream = imeSession.openEventStream(); 1186 1187 final var activity = createTestActivity(SOFT_INPUT_STATE_ALWAYS_HIDDEN); 1188 final var decorView = activity.getWindow().getDecorView(); 1189 int imeHeight = decorView.getRootWindowInsets().getInsets(ime()).bottom; 1190 1191 assertEquals(0, imeHeight); 1192 1193 imeSession.callRequestShowSelf(0 /* flags */); 1194 1195 PollingCheck.waitFor(TIMEOUT, () -> decorView.getRootWindowInsets().isVisible(ime())); 1196 1197 imeHeight = decorView.getRootWindowInsets().getInsets(ime()).bottom; 1198 final boolean isFullscreen = expectCommand(stream, 1199 imeSession.callGetOnEvaluateFullscreenMode(), TIMEOUT) 1200 .getReturnBooleanValue(); 1201 assertEquals(isFullscreen, useFullscreenMode); 1202 if (isFullscreen) { 1203 // In Fullscreen mode the IME doesn't provide any insets. 1204 assertEquals("Height of ime: " + imeHeight + " should be zero in fullscreen mode", 1205 0, imeHeight); 1206 } else { 1207 final int imeNavBarHeight = expectCommand(stream, 1208 imeSession.callGetImeCaptionBarHeight(), TIMEOUT) 1209 .getReturnIntegerValue(); 1210 assertTrue("Height of ime: " + imeHeight + " should be at least as big as" 1211 + " the height of the IME navigation bar: " + imeNavBarHeight, 1212 imeHeight >= imeNavBarHeight); 1213 } 1214 } 1215 } 1216 1217 /** Explicitly start-up the IME process if it would have been prevented. */ 1218 protected void ensureImeRunning() { 1219 if (isPreventImeStartup()) { 1220 createTestActivity(SOFT_INPUT_STATE_ALWAYS_VISIBLE); 1221 } 1222 } 1223 1224 /** Test case for committing and setting composing region after cursor. */ 1225 private static UpdateSelectionTest getCommitAndSetComposingRegionTest( 1226 long timeout, String makerPrefix) throws Exception { 1227 UpdateSelectionTest test = new UpdateSelectionTest(timeout, makerPrefix) { 1228 @Override 1229 public void testMethodImpl() throws Exception { 1230 // "abc|" 1231 expectCommand(stream, imeSession.callCommitText("abc", 1), timeout); 1232 verifyText("abc", 3, 3); 1233 final Bundle arguments1 = expectEvent(stream, 1234 eventMatcher("onUpdateSelection"), 1235 timeout).getArguments(); 1236 expectOnUpdateSelectionArguments(arguments1, 0, 0, 3, 3, -1, -1); 1237 notExpectEvent(stream, 1238 eventMatcher("onUpdateSelection"), 1239 EXPECTED_TIMEOUT); 1240 1241 // "|abc" 1242 expectCommand(stream, imeSession.callSetSelection(0, 0), timeout); 1243 verifyText("abc", 0, 0); 1244 final Bundle arguments2 = expectEvent(stream, 1245 eventMatcher("onUpdateSelection"), 1246 timeout).getArguments(); 1247 expectOnUpdateSelectionArguments(arguments2, 3, 3, 0, 0, -1, -1); 1248 notExpectEvent(stream, 1249 eventMatcher("onUpdateSelection"), 1250 EXPECTED_TIMEOUT); 1251 1252 // "Back |abc" 1253 // --- 1254 expectCommand(stream, imeSession.callBeginBatchEdit(), timeout); 1255 expectCommand(stream, imeSession.callCommitText("Back ", 1), timeout); 1256 expectCommand(stream, imeSession.callSetComposingRegion(5, 8), timeout); 1257 expectCommand(stream, imeSession.callEndBatchEdit(), timeout); 1258 verifyText("Back abc", 5, 5); 1259 final Bundle arguments3 = expectEvent(stream, 1260 eventMatcher("onUpdateSelection"), 1261 timeout).getArguments(); 1262 expectOnUpdateSelectionArguments(arguments3, 0, 0, 5, 5, 5, 8); 1263 notExpectEvent(stream, 1264 eventMatcher("onUpdateSelection"), 1265 EXPECTED_TIMEOUT); 1266 } 1267 }; 1268 return test; 1269 } 1270 1271 /** Test case for committing space and setting composing region after cursor. */ 1272 private static UpdateSelectionTest getCommitSpaceAndSetComposingRegionTest( 1273 long timeout, String makerPrefix) throws Exception { 1274 UpdateSelectionTest test = new UpdateSelectionTest(timeout, makerPrefix) { 1275 @Override 1276 public void testMethodImpl() throws Exception { 1277 // "Hello|" 1278 // ----- 1279 expectCommand(stream, imeSession.callSetComposingText("Hello", 1), timeout); 1280 verifyText("Hello", 5, 5); 1281 final Bundle arguments1 = expectEvent(stream, 1282 eventMatcher("onUpdateSelection"), 1283 timeout).getArguments(); 1284 expectOnUpdateSelectionArguments(arguments1, 0, 0, 5, 5, 0, 5); 1285 notExpectEvent(stream, 1286 eventMatcher("onUpdateSelection"), 1287 EXPECTED_TIMEOUT); 1288 1289 // "|Hello" 1290 // ----- 1291 expectCommand(stream, imeSession.callSetSelection(0, 0), timeout); 1292 verifyText("Hello", 0, 0); 1293 final Bundle arguments2 = expectEvent(stream, 1294 eventMatcher("onUpdateSelection"), 1295 timeout).getArguments(); 1296 expectOnUpdateSelectionArguments(arguments2, 5, 5, 0, 0, 0, 5); 1297 notExpectEvent(stream, 1298 eventMatcher("onUpdateSelection"), 1299 EXPECTED_TIMEOUT); 1300 1301 // " |Hello" 1302 // ----- 1303 expectCommand(stream, imeSession.callBeginBatchEdit(), timeout); 1304 expectCommand(stream, imeSession.callFinishComposingText(), timeout); 1305 expectCommand(stream, imeSession.callCommitText(" ", 1), timeout); 1306 expectCommand(stream, imeSession.callSetComposingRegion(1, 6), timeout); 1307 expectCommand(stream, imeSession.callEndBatchEdit(), timeout); 1308 1309 verifyText(" Hello", 1, 1); 1310 final Bundle arguments3 = expectEvent(stream, 1311 eventMatcher("onUpdateSelection"), 1312 timeout).getArguments(); 1313 expectOnUpdateSelectionArguments(arguments3, 0, 0, 1, 1, 1, 6); 1314 notExpectEvent(stream, 1315 eventMatcher("onUpdateSelection"), 1316 EXPECTED_TIMEOUT); 1317 } 1318 }; 1319 return test; 1320 } 1321 1322 /** 1323 * Test case for committing space in the middle of selection and setting composing region after 1324 * cursor. 1325 */ 1326 private static UpdateSelectionTest getCommitSpaceAndSetComposingRegionInSelectionTest( 1327 long timeout, String makerPrefix) throws Exception { 1328 UpdateSelectionTest test = new UpdateSelectionTest(timeout, makerPrefix) { 1329 @Override 1330 public void testMethodImpl() throws Exception { 1331 // "2005abc|" 1332 expectCommand(stream, imeSession.callCommitText("2005abc", 1), timeout); 1333 verifyText("2005abc", 7, 7); 1334 final Bundle arguments1 = expectEvent(stream, 1335 eventMatcher("onUpdateSelection"), 1336 timeout).getArguments(); 1337 expectOnUpdateSelectionArguments(arguments1, 0, 0, 7, 7, -1, -1); 1338 notExpectEvent(stream, 1339 eventMatcher("onUpdateSelection"), 1340 EXPECTED_TIMEOUT); 1341 1342 // "2005|abc" 1343 expectCommand(stream, imeSession.callSetSelection(4, 4), timeout); 1344 verifyText("2005abc", 4, 4); 1345 final Bundle arguments2 = expectEvent(stream, 1346 eventMatcher("onUpdateSelection"), 1347 timeout).getArguments(); 1348 expectOnUpdateSelectionArguments(arguments2, 7, 7, 4, 4, -1, -1); 1349 notExpectEvent(stream, 1350 eventMatcher("onUpdateSelection"), 1351 EXPECTED_TIMEOUT); 1352 1353 // "2005 |abc" 1354 // --- 1355 expectCommand(stream, imeSession.callBeginBatchEdit(), timeout); 1356 expectCommand(stream, imeSession.callCommitText(" ", 1), timeout); 1357 expectCommand(stream, imeSession.callSetComposingRegion(5, 8), timeout); 1358 expectCommand(stream, imeSession.callEndBatchEdit(), timeout); 1359 1360 verifyText("2005 abc", 5, 5); 1361 final Bundle arguments3 = expectEvent(stream, 1362 eventMatcher("onUpdateSelection"), 1363 timeout).getArguments(); 1364 expectOnUpdateSelectionArguments(arguments3, 4, 4, 5, 5, 5, 8); 1365 notExpectEvent(stream, 1366 eventMatcher("onUpdateSelection"), 1367 EXPECTED_TIMEOUT); 1368 } 1369 }; 1370 return test; 1371 } 1372 1373 private static void expectOnUpdateSelectionArguments(Bundle arguments, 1374 int expectedOldSelStart, int expectedOldSelEnd, int expectedNewSelStart, 1375 int expectedNewSelEnd, int expectedCandidateStart, int expectedCandidateEnd) { 1376 assertEquals(expectedOldSelStart, arguments.getInt("oldSelStart")); 1377 assertEquals(expectedOldSelEnd, arguments.getInt("oldSelEnd")); 1378 assertEquals(expectedNewSelStart, arguments.getInt("newSelStart")); 1379 assertEquals(expectedNewSelEnd, arguments.getInt("newSelEnd")); 1380 assertEquals(expectedCandidateStart, arguments.getInt("candidatesStart")); 1381 assertEquals(expectedCandidateEnd, arguments.getInt("candidatesEnd")); 1382 } 1383 1384 /** 1385 * Helper class for wrapping tests for {@link android.widget.TextView} and @{@link WebView} 1386 * relates to batch edit and update selection change. 1387 */ 1388 private abstract static class UpdateSelectionTest { 1389 private final long mTimeout; 1390 private final String mMaker; 1391 private final AtomicReference<EditText> mEditTextRef = new AtomicReference<>(); 1392 private final AtomicReference<UiObject2> mInputTextFieldRef = new AtomicReference<>(); 1393 1394 public final MockImeSession imeSession; 1395 public final ImeEventStream stream; 1396 1397 // True if testing TextView, otherwise test WebView 1398 private boolean mIsTestingTextView; 1399 1400 UpdateSelectionTest(long timeout, String makerPrefix) throws Exception { 1401 this.mTimeout = timeout; 1402 this.mMaker = makerPrefix + SystemClock.elapsedRealtimeNanos(); 1403 imeSession = MockImeSession.create( 1404 InstrumentationRegistry.getInstrumentation().getContext(), 1405 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 1406 new ImeSettings.Builder()); 1407 stream = imeSession.openEventStream(); 1408 } 1409 1410 /** 1411 * Runs the real test logic, which would test onStartInput event first, then test the logic 1412 * in {@link #testMethodImpl()}. 1413 * 1414 * @throws Exception if timeout or assert fails 1415 */ 1416 public void runTest() throws Exception { 1417 if (mIsTestingTextView) { 1418 TestActivity.startSync(activity -> { 1419 final LinearLayout layout = new LinearLayout(activity); 1420 layout.setOrientation(LinearLayout.VERTICAL); 1421 final EditText editText = new EditText(activity); 1422 layout.addView(editText); 1423 editText.requestFocus(); 1424 editText.setPrivateImeOptions(mMaker); 1425 mEditTextRef.set(editText); 1426 return layout; 1427 }); 1428 assertNotNull(mEditTextRef.get()); 1429 } else { 1430 final UiObject2 inputTextField = TestWebView.launchTestWebViewActivity( 1431 mTimeout, mMaker); 1432 assertNotNull("Editor must exists on WebView", inputTextField); 1433 mInputTextFieldRef.set(inputTextField); 1434 inputTextField.click(); 1435 } 1436 expectEvent(stream, editorMatcher("onStartInput", mMaker), TIMEOUT); 1437 1438 // Code for testing input connection logic. 1439 testMethodImpl(); 1440 } 1441 1442 /** 1443 * Test method to be overridden by implementation class. 1444 */ 1445 public abstract void testMethodImpl() throws Exception; 1446 1447 /** 1448 * Verifies text and selection range in the edit text if this is running tests for TextView; 1449 * otherwise verifies the text (no selection) in the WebView. 1450 * @param expectedText expected text in the TextView or WebView 1451 * @param selStart expected start position of the selection in the TextView; will be ignored 1452 * for WebView 1453 * @param selEnd expected end position of the selection in the WebView; will be ignored for 1454 * WebView 1455 * @throws Exception if timeout or assert fails 1456 */ 1457 public void verifyText(String expectedText, int selStart, int selEnd) throws Exception { 1458 if (mIsTestingTextView) { 1459 EditText editText = mEditTextRef.get(); 1460 assertNotNull(editText); 1461 waitOnMainUntil(()-> 1462 expectedText.equals(editText.getText().toString()) 1463 && selStart == editText.getSelectionStart() 1464 && selEnd == editText.getSelectionEnd(), mTimeout); 1465 } else { 1466 UiObject2 inputTextField = mInputTextFieldRef.get(); 1467 assertNotNull(inputTextField); 1468 waitOnMainUntil(()-> expectedText.equals(inputTextField.getText()), mTimeout); 1469 } 1470 } 1471 1472 public UpdateSelectionTest setTestTextView(boolean isTestingTextView) { 1473 this.mIsTestingTextView = isTestingTextView; 1474 return this; 1475 } 1476 } 1477 1478 private static WindowLayoutInfoParcelable verifyReceivedWindowLayout(ImeEventStream stream) 1479 throws TimeoutException { 1480 final ImeEvent imeEvent = expectEvent(stream, 1481 eventMatcher("getWindowLayoutInfo"), 1482 TIMEOUT); 1483 return imeEvent.getArguments() 1484 .getParcelable("WindowLayoutInfo", WindowLayoutInfoParcelable.class); 1485 } 1486 1487 /** 1488 * Checks if the resizing of a rectangle maintains a specified aspect ratio. 1489 * <p> 1490 * This function compares the initial and resized dimensions of a rectangle, using a provided 1491 * function ({@code getSize}) to extract the relevant dimension (e.g., width or height). 1492 * It determines if the resized dimension matches the expected value, calculated as the 1493 * initial dimension multiplied by the given ratio. 1494 * <p> 1495 * If the aspect ratio is not maintained, an error message detailing the discrepancy is 1496 * appended to the {@code outErrorMessage} StringBuilder. 1497 * 1498 * @param getSize a function that extracts the relevant dimension (width or height) from a Rect. 1499 * @param initial the initial Rect before resizing. 1500 * @param resized the Rect after resizing. 1501 * @param ratio the expected resize ratio (e.g., 0.8 for a 20% decrease). 1502 * @param outErrorMessage a StringBuilder to which an error message is appended if the aspect 1503 * ratio is not maintained. 1504 * @return {@code true} if the resize maintains the specified ratio, {@code false} otherwise. 1505 */ 1506 private static boolean isResizedWithRatio(@NonNull Function<Rect, Integer> getSize, 1507 @NonNull Rect initial, @NonNull Rect resized, double ratio, 1508 @NonNull StringBuilder outErrorMessage) { 1509 // Align with the rounding approach in DisplayMetricsSession#changeDisplayMetrics. 1510 final int expected = (int) (getSize.apply(initial) * ratio); 1511 final int actual = getSize.apply(resized); 1512 final boolean isCorrect = (expected == actual); 1513 if (!isCorrect) { 1514 outErrorMessage 1515 .append("\n isResizedWithRatio(initial=") 1516 .append(initial) 1517 .append(", resized=") 1518 .append(resized) 1519 .append(", ratio=") 1520 .append(ratio) 1521 .append("):\n expected size: ") 1522 .append(expected) 1523 .append(" but was: ") 1524 .append(actual); 1525 } 1526 return isCorrect; 1527 } 1528 } 1529