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.provider.Settings.Secure.STYLUS_HANDWRITING_DEFAULT_VALUE; 20 import static android.provider.Settings.Secure.STYLUS_HANDWRITING_ENABLED; 21 import static android.view.inputmethod.ConnectionlessHandwritingCallback.CONNECTIONLESS_HANDWRITING_ERROR_NO_TEXT_RECOGNIZED; 22 import static android.view.inputmethod.ConnectionlessHandwritingCallback.CONNECTIONLESS_HANDWRITING_ERROR_UNSUPPORTED; 23 import static android.view.inputmethod.Flags.FLAG_ADAPTIVE_HANDWRITING_BOUNDS; 24 import static android.view.inputmethod.Flags.FLAG_CONNECTIONLESS_HANDWRITING; 25 import static android.view.inputmethod.Flags.FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR; 26 import static android.view.inputmethod.Flags.FLAG_INITIATION_WITHOUT_INPUT_CONNECTION; 27 import static android.view.inputmethod.Flags.FLAG_USE_HANDWRITING_LISTENER_FOR_TOOLTYPE; 28 import static android.view.inputmethod.InputMethodInfo.ACTION_STYLUS_HANDWRITING_SETTINGS; 29 30 import static com.android.cts.mockime.ImeEventStreamTestUtils.editorMatcher; 31 import static com.android.cts.mockime.ImeEventStreamTestUtils.eventMatcher; 32 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectBindInput; 33 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectCommand; 34 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent; 35 import static com.android.cts.mockime.ImeEventStreamTestUtils.notExpectEvent; 36 import static com.android.cts.mockime.ImeEventStreamTestUtils.withDescription; 37 import static com.android.input.flags.Flags.FLAG_DEVICE_ASSOCIATIONS; 38 import static com.android.text.flags.Flags.FLAG_HANDWRITING_END_OF_LINE_TAP; 39 import static com.android.text.flags.Flags.FLAG_HANDWRITING_TRACK_DISABLED; 40 import static com.android.text.flags.Flags.FLAG_HANDWRITING_UNSUPPORTED_MESSAGE; 41 import static com.android.text.flags.Flags.FLAG_HANDWRITING_UNSUPPORTED_SHOW_SOFT_INPUT_FIX; 42 43 import static com.google.common.truth.Truth.assertThat; 44 45 import static org.junit.Assert.assertEquals; 46 import static org.junit.Assert.assertFalse; 47 import static org.junit.Assert.assertNotNull; 48 import static org.junit.Assert.assertTrue; 49 import static org.junit.Assume.assumeFalse; 50 import static org.junit.Assume.assumeTrue; 51 import static org.mockito.Mockito.mock; 52 53 import android.Manifest; 54 import android.app.Activity; 55 import android.app.Instrumentation; 56 import android.content.ComponentName; 57 import android.content.Context; 58 import android.content.Intent; 59 import android.content.pm.PackageManager; 60 import android.graphics.Color; 61 import android.graphics.Region; 62 import android.hardware.input.InputManager; 63 import android.inputmethodservice.InputMethodService; 64 import android.os.Process; 65 import android.platform.test.annotations.AppModeFull; 66 import android.platform.test.annotations.AppModeSdkSandbox; 67 import android.platform.test.annotations.RequiresFlagsEnabled; 68 import android.platform.test.flag.junit.CheckFlagsRule; 69 import android.platform.test.flag.junit.DeviceFlagsValueProvider; 70 import android.provider.Settings; 71 import android.text.InputType; 72 import android.text.TextUtils; 73 import android.util.DisplayMetrics; 74 import android.util.Pair; 75 import android.view.Display; 76 import android.view.InputDevice; 77 import android.view.KeyEvent; 78 import android.view.MotionEvent; 79 import android.view.View; 80 import android.view.ViewConfiguration; 81 import android.view.ViewGroup; 82 import android.view.inputmethod.ConnectionlessHandwritingCallback; 83 import android.view.inputmethod.CursorAnchorInfo; 84 import android.view.inputmethod.EditorInfo; 85 import android.view.inputmethod.InputConnection; 86 import android.view.inputmethod.InputMethodInfo; 87 import android.view.inputmethod.InputMethodManager; 88 import android.view.inputmethod.cts.util.EndToEndImeTestBase; 89 import android.view.inputmethod.cts.util.MockTestActivityUtil; 90 import android.view.inputmethod.cts.util.NoOpInputConnection; 91 import android.view.inputmethod.cts.util.TestActivity; 92 import android.view.inputmethod.cts.util.TestActivity2; 93 import android.view.inputmethod.cts.util.TestUtils; 94 import android.widget.EditText; 95 import android.widget.LinearLayout; 96 97 import androidx.annotation.ColorInt; 98 import androidx.annotation.NonNull; 99 import androidx.test.filters.FlakyTest; 100 import androidx.test.platform.app.InstrumentationRegistry; 101 102 import com.android.compatibility.common.util.ApiTest; 103 import com.android.compatibility.common.util.CommonTestUtils; 104 import com.android.compatibility.common.util.GestureNavSwitchHelper; 105 import com.android.compatibility.common.util.SystemUtil; 106 import com.android.cts.input.DebugInputRule; 107 import com.android.cts.input.UinputStylus; 108 import com.android.cts.input.UinputTouchDevice; 109 import com.android.cts.input.UinputTouchScreen; 110 import com.android.cts.mockime.ImeEvent; 111 import com.android.cts.mockime.ImeEventStream; 112 import com.android.cts.mockime.ImeEventStreamTestUtils.DescribedPredicate; 113 import com.android.cts.mockime.ImeSettings; 114 import com.android.cts.mockime.MockImeSession; 115 116 import org.junit.After; 117 import org.junit.Before; 118 import org.junit.Rule; 119 import org.junit.Test; 120 121 import java.util.ArrayList; 122 import java.util.Iterator; 123 import java.util.List; 124 import java.util.concurrent.CountDownLatch; 125 import java.util.concurrent.TimeUnit; 126 import java.util.concurrent.TimeoutException; 127 import java.util.concurrent.atomic.AtomicReference; 128 import java.util.function.Predicate; 129 130 /** 131 * IMF and end-to-end Stylus handwriting tests. 132 */ 133 @AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).") 134 public class StylusHandwritingTest extends EndToEndImeTestBase { 135 private static final long TIMEOUT_IN_SECONDS = 5; 136 private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(TIMEOUT_IN_SECONDS); 137 private static final long TIMEOUT_6_S = TimeUnit.SECONDS.toMillis(6); 138 private static final long TIMEOUT_1_S = TimeUnit.SECONDS.toMillis(1); 139 private static final long NOT_EXPECT_TIMEOUT_IN_SECONDS = 3; 140 private static final long NOT_EXPECT_TIMEOUT = 141 TimeUnit.SECONDS.toMillis(NOT_EXPECT_TIMEOUT_IN_SECONDS); 142 private static final int SETTING_VALUE_ON = 1; 143 private static final int SETTING_VALUE_OFF = 0; 144 private static final int HANDWRITING_BOUNDS_OFFSET_PX = 20; 145 // A timeout greater than HandwritingModeController#HANDWRITING_DELEGATION_IDLE_TIMEOUT_MS. 146 private static final long DELEGATION_AFTER_IDLE_TIMEOUT_MS = 3100; 147 private static final int NUMBER_OF_INJECTED_EVENTS = 5; 148 private static final String TEST_LAUNCHER_COMPONENT = 149 "android.view.inputmethod.ctstestlauncher/" 150 + "android.view.inputmethod.ctstestlauncher.LauncherActivity"; 151 152 private Context mContext; 153 private int mHwInitialState; 154 private boolean mShouldRestoreInitialHwState; 155 private String mDefaultLauncherToRestore; 156 157 private static final GestureNavSwitchHelper sGestureNavRule = new GestureNavSwitchHelper(); 158 159 private final DeviceFlagsValueProvider mFlagsValueProvider = new DeviceFlagsValueProvider(); 160 161 @Rule 162 public final CheckFlagsRule mCheckFlagsRule = new CheckFlagsRule(mFlagsValueProvider); 163 164 @Rule public final DebugInputRule debugInputRule = new DebugInputRule(); 165 166 @Before setup()167 public void setup() { 168 mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 169 assumeFalse(mContext.getPackageManager().hasSystemFeature( 170 PackageManager.FEATURE_LEANBACK_ONLY)); 171 assumeFalse(mContext.getPackageManager().hasSystemFeature( 172 PackageManager.FEATURE_AUTOMOTIVE)); 173 174 mHwInitialState = Settings.Secure.getInt(mContext.getContentResolver(), 175 STYLUS_HANDWRITING_ENABLED, STYLUS_HANDWRITING_DEFAULT_VALUE); 176 if (mHwInitialState != SETTING_VALUE_ON) { 177 SystemUtil.runWithShellPermissionIdentity(() -> { 178 Settings.Secure.putInt(mContext.getContentResolver(), 179 STYLUS_HANDWRITING_ENABLED, SETTING_VALUE_ON); 180 }, Manifest.permission.WRITE_SECURE_SETTINGS); 181 mShouldRestoreInitialHwState = true; 182 } 183 } 184 185 @After tearDown()186 public void tearDown() { 187 MockTestActivityUtil.forceStopPackage(); 188 if (mShouldRestoreInitialHwState) { 189 mShouldRestoreInitialHwState = false; 190 SystemUtil.runWithShellPermissionIdentity(() -> { 191 Settings.Secure.putInt(mContext.getContentResolver(), 192 STYLUS_HANDWRITING_ENABLED, mHwInitialState); 193 }, Manifest.permission.WRITE_SECURE_SETTINGS); 194 } 195 if (mDefaultLauncherToRestore != null) { 196 setDefaultLauncher(mDefaultLauncherToRestore); 197 mDefaultLauncherToRestore = null; 198 } 199 } 200 201 /** 202 * Verify current IME has {@link InputMethodInfo} for stylus handwriting, settings. 203 */ 204 @Test 205 @ApiTest(apis = {"android.view.inputmethod.InputMethodInfo#supportsStylusHandwriting", 206 "android.view.inputmethod.InputMethodInfo#ACTION_STYLUS_HANDWRITING_SETTINGS", 207 "android.view.inputmethod.InputMethodInfo#createStylusHandwritingSettingsActivityIntent" 208 }) testHandwritingInfo()209 public void testHandwritingInfo() throws Exception { 210 try (MockImeSession imeSession = MockImeSession.create( 211 InstrumentationRegistry.getInstrumentation().getContext(), 212 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 213 new ImeSettings.Builder())) { 214 InputMethodInfo info = imeSession.getInputMethodInfo(); 215 assertTrue(info.supportsStylusHandwriting()); 216 // TODO(b/217957587): migrate CtsMockInputMethodLib to android_library and use 217 // string resource. 218 Intent stylusSettingsIntent = info.createStylusHandwritingSettingsActivityIntent(); 219 assertEquals(ACTION_STYLUS_HANDWRITING_SETTINGS, stylusSettingsIntent.getAction()); 220 assertEquals("handwriting_settings", 221 stylusSettingsIntent.getComponent().getClassName()); 222 } 223 } 224 225 @Test testIsStylusHandwritingAvailable_prefDisabled()226 public void testIsStylusHandwritingAvailable_prefDisabled() throws Exception { 227 try (MockImeSession imeSession = MockImeSession.create( 228 InstrumentationRegistry.getInstrumentation().getContext(), 229 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 230 new ImeSettings.Builder())) { 231 imeSession.openEventStream(); 232 233 // Disable pref 234 SystemUtil.runWithShellPermissionIdentity(() -> { 235 Settings.Secure.putInt(mContext.getContentResolver(), 236 STYLUS_HANDWRITING_ENABLED, SETTING_VALUE_OFF); 237 }, Manifest.permission.WRITE_SECURE_SETTINGS); 238 mShouldRestoreInitialHwState = true; 239 240 launchTestActivity(getTestMarker()); 241 assertFalse( 242 "should return false for isStylusHandwritingAvailable() when pref is disabled", 243 mContext.getSystemService( 244 InputMethodManager.class).isStylusHandwritingAvailable()); 245 } 246 } 247 248 @Test testIsStylusHandwritingAvailable()249 public void testIsStylusHandwritingAvailable() throws Exception { 250 try (MockImeSession imeSession = MockImeSession.create( 251 InstrumentationRegistry.getInstrumentation().getContext(), 252 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 253 new ImeSettings.Builder())) { 254 imeSession.openEventStream(); 255 256 launchTestActivity(getTestMarker()); 257 assertTrue("Mock IME should return true for isStylusHandwritingAvailable() ", 258 mContext.getSystemService( 259 InputMethodManager.class).isStylusHandwritingAvailable()); 260 } 261 } 262 263 @Test 264 @RequiresFlagsEnabled(FLAG_CONNECTIONLESS_HANDWRITING) testIsConnectionlessStylusHandwritingAvailable_prefDisabled()265 public void testIsConnectionlessStylusHandwritingAvailable_prefDisabled() throws Exception { 266 try (MockImeSession imeSession = MockImeSession.create( 267 InstrumentationRegistry.getInstrumentation().getContext(), 268 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 269 new ImeSettings.Builder())) { 270 imeSession.openEventStream(); 271 272 // Disable pref 273 SystemUtil.runWithShellPermissionIdentity(() -> { 274 Settings.Secure.putInt(mContext.getContentResolver(), 275 STYLUS_HANDWRITING_ENABLED, SETTING_VALUE_OFF); 276 }, Manifest.permission.WRITE_SECURE_SETTINGS); 277 mShouldRestoreInitialHwState = true; 278 279 launchTestActivity(getTestMarker()); 280 assertFalse( 281 "Mock IME should return false for isConnectionlessStylusHandwritingAvailable() " 282 + "when pref is disabled", 283 mContext.getSystemService( 284 InputMethodManager.class).isConnectionlessStylusHandwritingAvailable()); 285 } 286 } 287 288 @Test 289 @RequiresFlagsEnabled(FLAG_CONNECTIONLESS_HANDWRITING) testIsConnectionlessStylusHandwritingAvailable()290 public void testIsConnectionlessStylusHandwritingAvailable() throws Exception { 291 try (MockImeSession imeSession = MockImeSession.create( 292 InstrumentationRegistry.getInstrumentation().getContext(), 293 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 294 new ImeSettings.Builder())) { 295 imeSession.openEventStream(); 296 297 launchTestActivity(getTestMarker()); 298 assertTrue( 299 "Mock IME should return true for isConnectionlessStylusHandwritingAvailable()", 300 mContext.getSystemService( 301 InputMethodManager.class).isConnectionlessStylusHandwritingAvailable()); 302 } 303 } 304 305 /** 306 * Test to verify that we dont init handwriting on devices that dont have any supported stylus. 307 */ 308 @Test testHandwritingNoInitOnDeviceWithNoStylus()309 public void testHandwritingNoInitOnDeviceWithNoStylus() { 310 assumeTrue("Skipping test on devices that do not have stylus support", 311 hasSupportedStylus()); 312 final InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); 313 try (MockImeSession imeSession = MockImeSession.create( 314 InstrumentationRegistry.getInstrumentation().getContext(), 315 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 316 new ImeSettings.Builder())) { 317 final ImeEventStream stream = imeSession.openEventStream(); 318 final String marker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 319 final EditText editText = launchTestActivity(marker); 320 imm.startStylusHandwriting(editText); 321 // Handwriting should not start since there are no stylus devices registered. 322 notExpectEvent( 323 stream, 324 editorMatcher("onStartStylusHandwriting", marker), 325 NOT_EXPECT_TIMEOUT); 326 } catch (Exception e) { 327 } 328 } 329 330 @Test testHandwritingDoesNotStartWhenNoStylusDown()331 public void testHandwritingDoesNotStartWhenNoStylusDown() throws Exception { 332 final InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); 333 try (MockImeSession imeSession = MockImeSession.create( 334 InstrumentationRegistry.getInstrumentation().getContext(), 335 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 336 new ImeSettings.Builder())) { 337 final ImeEventStream stream = imeSession.openEventStream(); 338 339 final String marker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 340 final EditText editText = launchTestActivity(marker); 341 342 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 343 notExpectEvent( 344 stream, 345 editorMatcher("onStartInputView", marker), 346 NOT_EXPECT_TIMEOUT); 347 348 addVirtualStylusIdForTestSession(); 349 imm.startStylusHandwriting(editText); 350 351 // Handwriting should not start 352 notExpectEvent( 353 stream, 354 editorMatcher("onStartStylusHandwriting", marker), 355 NOT_EXPECT_TIMEOUT); 356 357 verifyStylusHandwritingWindowIsNotShown(stream, imeSession); 358 } 359 } 360 361 @Test testHandwritingStartAndFinish()362 public void testHandwritingStartAndFinish() throws Exception { 363 final InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); 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(FOCUSED_EDIT_TEXT_TAG); 371 final EditText editText = launchTestActivity(marker); 372 373 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 374 notExpectEvent( 375 stream, 376 editorMatcher("onStartInputView", marker), 377 NOT_EXPECT_TIMEOUT); 378 379 addVirtualStylusIdForTestSession(); 380 // Touch down with a stylus 381 final int startX = editText.getWidth() / 2; 382 final int startY = editText.getHeight() / 2; 383 TestUtils.injectStylusDownEvent(editText, startX, startY); 384 385 try { 386 imm.startStylusHandwriting(editText); 387 // keyboard shouldn't show up. 388 notExpectEvent( 389 stream, 390 editorMatcher("onStartInputView", marker), 391 NOT_EXPECT_TIMEOUT); 392 393 // Handwriting should start 394 expectEvent( 395 stream, 396 editorMatcher("onPrepareStylusHandwriting", marker), 397 TIMEOUT); 398 expectEvent( 399 stream, 400 editorMatcher("onStartStylusHandwriting", marker), 401 TIMEOUT); 402 403 verifyStylusHandwritingWindowIsShown(stream, imeSession); 404 } finally { 405 // Release the stylus pointer 406 TestUtils.injectStylusUpEvent(editText, startX, startY); 407 } 408 409 // Verify calling finishStylusHandwriting() calls onFinishStylusHandwriting(). 410 imeSession.callFinishStylusHandwriting(); 411 expectEvent( 412 stream, 413 editorMatcher("onFinishStylusHandwriting", marker), 414 TIMEOUT); 415 } 416 } 417 418 /** 419 * Verifies that stylus hover events initializes the InkWindow. 420 * @throws Exception 421 */ 422 @Test testStylusHoverInitInkWindow()423 public void testStylusHoverInitInkWindow() throws Exception { 424 try (MockImeSession imeSession = MockImeSession.create( 425 InstrumentationRegistry.getInstrumentation().getContext(), 426 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 427 new ImeSettings.Builder())) { 428 final ImeEventStream stream = imeSession.openEventStream(); 429 430 final String marker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 431 final EditText editText = launchTestActivity(marker); 432 433 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 434 notExpectEvent( 435 stream, 436 editorMatcher("onStartInputView", marker), 437 NOT_EXPECT_TIMEOUT); 438 439 addVirtualStylusIdForTestSession(); 440 // Verify there is no handwriting window before stylus is added. 441 assertFalse(expectCommand( 442 stream, imeSession.callHasStylusHandwritingWindow(), TIMEOUT_1_S) 443 .getReturnBooleanValue()); 444 // Stylus hover 445 final int startX = editText.getWidth() / 2; 446 final int startY = editText.getHeight() / 2; 447 TestUtils.injectStylusHoverEvents(editText, startX, startY); 448 // keyboard shouldn't show up. 449 notExpectEvent( 450 stream, 451 editorMatcher("onStartInputView", marker), 452 NOT_EXPECT_TIMEOUT); 453 454 // Handwriting prep should start for stylus onHover 455 expectEvent( 456 stream, 457 editorMatcher("onPrepareStylusHandwriting", marker), 458 TIMEOUT); 459 notExpectEvent( 460 stream, 461 editorMatcher("onStartStylusHandwriting", marker), 462 NOT_EXPECT_TIMEOUT); 463 464 // Verify handwriting window exists but not shown. 465 assertTrue(expectCommand( 466 stream, imeSession.callHasStylusHandwritingWindow(), TIMEOUT_1_S) 467 .getReturnBooleanValue()); 468 verifyStylusHandwritingWindowIsNotShown(stream, imeSession); 469 } 470 } 471 472 /** 473 * Call {@link InputMethodManager#startStylusHandwriting(View)} and inject Stylus touch events 474 * on screen. Make sure {@link InputMethodService#onStylusHandwritingMotionEvent(MotionEvent)} 475 * receives those events via Spy window surface. 476 * @throws Exception 477 */ 478 @Test testHandwritingStylusEvents_onStylusHandwritingMotionEvent()479 public void testHandwritingStylusEvents_onStylusHandwritingMotionEvent() throws Exception { 480 testHandwritingStylusEvents(false /* verifyOnInkView */); 481 } 482 483 /** 484 * Call {@link InputMethodManager#startStylusHandwriting(View)} and inject Stylus touch events 485 * on screen. Make sure Inking view receives those events via Spy window surface. 486 * @throws Exception 487 */ 488 @Test testHandwritingStylusEvents_dispatchToInkView()489 public void testHandwritingStylusEvents_dispatchToInkView() throws Exception { 490 testHandwritingStylusEvents(false /* verifyOnInkView */); 491 } 492 verifyStylusHandwritingWindowIsShown(ImeEventStream stream, MockImeSession imeSession)493 private void verifyStylusHandwritingWindowIsShown(ImeEventStream stream, 494 MockImeSession imeSession) throws InterruptedException, TimeoutException { 495 CommonTestUtils.waitUntil("Stylus handwriting window should be shown", TIMEOUT_IN_SECONDS, 496 () -> expectCommand( 497 stream, imeSession.callGetStylusHandwritingWindowVisibility(), TIMEOUT) 498 .getReturnBooleanValue()); 499 } 500 verifyStylusHandwritingWindowIsNotShown(ImeEventStream stream, MockImeSession imeSession)501 private void verifyStylusHandwritingWindowIsNotShown(ImeEventStream stream, 502 MockImeSession imeSession) throws InterruptedException, TimeoutException { 503 CommonTestUtils.waitUntil("Stylus handwriting window should not be shown", 504 NOT_EXPECT_TIMEOUT_IN_SECONDS, 505 () -> !expectCommand( 506 stream, imeSession.callGetStylusHandwritingWindowVisibility(), TIMEOUT) 507 .getReturnBooleanValue()); 508 } 509 testHandwritingStylusEvents(boolean verifyOnInkView)510 private void testHandwritingStylusEvents(boolean verifyOnInkView) throws Exception { 511 final InputMethodManager imm = InstrumentationRegistry.getInstrumentation() 512 .getTargetContext().getSystemService(InputMethodManager.class); 513 try (MockImeSession imeSession = MockImeSession.create( 514 InstrumentationRegistry.getInstrumentation().getContext(), 515 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 516 new ImeSettings.Builder())) { 517 final ImeEventStream stream = imeSession.openEventStream(); 518 519 final String marker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 520 final EditText editText = launchTestActivity(marker); 521 522 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 523 notExpectEvent( 524 stream, 525 editorMatcher("onStartInputView", marker), 526 NOT_EXPECT_TIMEOUT); 527 528 addVirtualStylusIdForTestSession(); 529 final List<MotionEvent> injectedEvents = new ArrayList<>(); 530 // Touch down with a stylus 531 final int touchSlop = getTouchSlop(); 532 final int startX = editText.getWidth() / 2; 533 final int startY = editText.getHeight() / 2; 534 final int endX = startX + 2 * touchSlop; 535 final int endY = startY; 536 final int number = 5; 537 injectedEvents.add(TestUtils.injectStylusDownEvent(editText, startX, startY)); 538 539 try { 540 imm.startStylusHandwriting(editText); 541 542 // Handwriting should start 543 expectEvent( 544 stream, 545 editorMatcher("onStartStylusHandwriting", marker), 546 TIMEOUT); 547 548 verifyStylusHandwritingWindowIsShown(stream, imeSession); 549 550 if (verifyOnInkView) { 551 // Set IME stylus Ink view 552 assertTrue(expectCommand( 553 stream, 554 imeSession.callSetStylusHandwritingInkView(), 555 TIMEOUT).getReturnBooleanValue()); 556 } 557 558 injectedEvents.addAll( 559 TestUtils.injectStylusMoveEvents(editText, startX, startY, endX, endY, 560 number)); 561 } finally { 562 injectedEvents.add(TestUtils.injectStylusUpEvent(editText, endX, endY)); 563 } 564 565 expectEvent(stream, eventMatcher("onStylusMotionEvent"), TIMEOUT); 566 567 // get Stylus events from Ink view, splitting any batched events. 568 final ArrayList<MotionEvent> capturedBatchedEvents = expectCommand( 569 stream, imeSession.callGetStylusHandwritingEvents(), TIMEOUT) 570 .getReturnParcelableArrayListValue(); 571 assertNotNull(capturedBatchedEvents); 572 final ArrayList<MotionEvent> capturedEvents = new ArrayList<>(); 573 capturedBatchedEvents.forEach( 574 e -> capturedEvents.addAll(TestUtils.splitBatchedMotionEvent(e))); 575 576 // captured events should be same as injected. 577 assertEquals(injectedEvents.size(), capturedEvents.size()); 578 579 // Verify MotionEvents as well. 580 // Note: we cannot just use equals() since some MotionEvent fields can change after 581 // dispatch. 582 Iterator<MotionEvent> capturedIt = capturedEvents.iterator(); 583 Iterator<MotionEvent> injectedIt = injectedEvents.iterator(); 584 while (injectedIt.hasNext() && capturedIt.hasNext()) { 585 MotionEvent injected = injectedIt.next(); 586 MotionEvent captured = capturedIt.next(); 587 assertEquals("X should be same for MotionEvent", injected.getX(), captured.getX(), 588 5.0f); 589 assertEquals("Y should be same for MotionEvent", injected.getY(), captured.getY(), 590 5.0f); 591 assertEquals("Action should be same for MotionEvent", 592 injected.getAction(), captured.getAction()); 593 } 594 } 595 } 596 597 @FlakyTest(bugId = 210039666) 598 @Test 599 /** 600 * Inject Stylus events on top of focused editor and verify Handwriting is started and InkWindow 601 * is displayed. 602 */ testHandwritingEndToEnd()603 public void testHandwritingEndToEnd() throws Exception { 604 try (MockImeSession imeSession = MockImeSession.create( 605 InstrumentationRegistry.getInstrumentation().getContext(), 606 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 607 new ImeSettings.Builder())) { 608 final ImeEventStream stream = imeSession.openEventStream(); 609 610 final String marker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 611 final EditText editText = launchTestActivity(marker); 612 613 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 614 notExpectEvent( 615 stream, 616 editorMatcher("onStartInputView", marker), 617 NOT_EXPECT_TIMEOUT); 618 619 addVirtualStylusIdForTestSession(); 620 621 injectStylusEventToEditorAndVerify(editText, stream, imeSession, marker, 622 true /* verifyHandwritingStart */, true /* verifyHandwritingWindowShown */, 623 false /* verifyHandwritingWindowNotShown */); 624 } 625 } 626 627 /** 628 * Inject stylus tap on a focused non-empty EditText and verify that handwriting is started. 629 */ 630 @Test 631 @RequiresFlagsEnabled(FLAG_HANDWRITING_END_OF_LINE_TAP) testHandwriting_endOfLineTap()632 public void testHandwriting_endOfLineTap() throws Exception { 633 try (MockImeSession imeSession = MockImeSession.create( 634 InstrumentationRegistry.getInstrumentation().getContext(), 635 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 636 new ImeSettings.Builder())) { 637 final ImeEventStream stream = imeSession.openEventStream(); 638 639 final String marker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 640 final EditText editText = launchTestActivity(marker); 641 editText.setText("a"); 642 editText.setSelection(1); 643 644 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 645 notExpectEvent(stream, editorMatcher("onStartInputView", marker), NOT_EXPECT_TIMEOUT); 646 647 addVirtualStylusIdForTestSession(); 648 649 // Stylus tap must be after the end of the line. 650 final int x = editText.getWidth() / 2; 651 final int y = editText.getHeight() / 2; 652 TestUtils.injectStylusDownEvent(editText, x, y); 653 TestUtils.injectStylusUpEvent(editText, x, y); 654 655 notExpectEvent(stream, editorMatcher("onStartInputView", marker), NOT_EXPECT_TIMEOUT); 656 expectEvent(stream, editorMatcher("onStartStylusHandwriting", marker), TIMEOUT); 657 verifyStylusHandwritingWindowIsShown(stream, imeSession); 658 } 659 } 660 661 @FlakyTest(bugId = 222840964) 662 @Test 663 /** 664 * Inject Stylus events on top of focused editor and verify Handwriting can be initiated 665 * multiple times. 666 */ testHandwritingInitMultipleTimes()667 public void testHandwritingInitMultipleTimes() throws Exception { 668 try (MockImeSession imeSession = MockImeSession.create( 669 InstrumentationRegistry.getInstrumentation().getContext(), 670 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 671 new ImeSettings.Builder())) { 672 final ImeEventStream stream = imeSession.openEventStream(); 673 674 final String marker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 675 final EditText editText = launchTestActivity(marker); 676 677 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 678 notExpectEvent( 679 stream, 680 editorMatcher("onStartInputView", marker), 681 NOT_EXPECT_TIMEOUT); 682 683 // Try to init handwriting for multiple times. 684 for (int i = 0; i < 3; ++i) { 685 addVirtualStylusIdForTestSession(); 686 687 injectStylusEventToEditorAndVerify(editText, stream, imeSession, marker, 688 true /* verifyHandwritingStart */, true /* verifyHandwritingWindowShown */, 689 false /* verifyHandwritingWindowNotShown */); 690 691 imeSession.callFinishStylusHandwriting(); 692 expectEvent( 693 stream, 694 editorMatcher("onFinishStylusHandwriting", marker), 695 TIMEOUT); 696 } 697 } 698 } 699 700 @Test 701 /** 702 * Inject Stylus events on top of focused editor's handwriting bounds and verify 703 * Handwriting is started and InkWindow is displayed. 704 */ testHandwritingInOffsetHandwritingBounds()705 public void testHandwritingInOffsetHandwritingBounds() throws Exception { 706 try (MockImeSession imeSession = MockImeSession.create( 707 InstrumentationRegistry.getInstrumentation().getContext(), 708 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 709 new ImeSettings.Builder())) { 710 final ImeEventStream stream = imeSession.openEventStream(); 711 712 final String marker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 713 final EditText editText = launchTestActivity(marker); 714 715 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 716 notExpectEvent( 717 stream, 718 editorMatcher("onStartInputView", marker), 719 NOT_EXPECT_TIMEOUT); 720 721 addVirtualStylusIdForTestSession(); 722 injectStylusEventToEditorAndVerify(editText, stream, imeSession, marker, 723 true /* verifyHandwritingStart */, true /* verifyHandwritingWindowShown */, 724 false /* verifyHandwritingWindowNotShown */); 725 } 726 } 727 728 /** 729 * Inject Stylus events on top of focused editor and verify Handwriting is started and then 730 * inject events on navbar to swipe to home and make sure motionEvents are consumed by 731 * Handwriting window. 732 */ 733 @Test testStylusSession_stylusWouldNotTriggerNavbarGestures()734 public void testStylusSession_stylusWouldNotTriggerNavbarGestures() throws Exception { 735 assumeTrue(sGestureNavRule.isGestureMode()); 736 final InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); 737 try (MockImeSession imeSession = MockImeSession.create( 738 InstrumentationRegistry.getInstrumentation().getContext(), 739 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 740 new ImeSettings.Builder())) { 741 final ImeEventStream stream = imeSession.openEventStream(); 742 743 final String marker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 744 final EditText editText = launchTestActivity(marker); 745 746 addVirtualStylusIdForTestSession(); 747 SystemUtil.runWithShellPermissionIdentity(() -> 748 imm.setStylusWindowIdleTimeoutForTest(TIMEOUT * 2)); 749 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 750 notExpectEvent( 751 stream, 752 editorMatcher("onStartInputView", marker), 753 NOT_EXPECT_TIMEOUT); 754 755 addVirtualStylusIdForTestSession(); 756 757 injectStylusEventToEditorAndVerify(editText, stream, imeSession, marker, 758 true /* verifyHandwritingStart */, 759 false /* verifyHandwritingWindowShown */, 760 false /* verifyHandwritingWindowNotShown */); 761 762 // Inject stylus swipe up on navbar. 763 TestUtils.injectNavBarToHomeGestureEvents( 764 ((Activity) editText.getContext()), MotionEvent.TOOL_TYPE_STYLUS); 765 766 // Handwriting is finished if navigation gesture is executed. 767 // Make sure handwriting isn't finished. 768 notExpectEvent( 769 stream, 770 editorMatcher("onFinishStylusHandwriting", marker), 771 TIMEOUT_1_S); 772 } 773 } 774 775 /** 776 * Inject Stylus events on top of focused editor and verify Handwriting is started and then 777 * inject finger touch events on navbar to swipe to home and make sure user can swipe to home. 778 */ 779 @Test testStylusSession_fingerTriggersNavbarGestures()780 public void testStylusSession_fingerTriggersNavbarGestures() throws Exception { 781 assumeTrue(sGestureNavRule.isGestureMode()); 782 783 try (MockImeSession imeSession = MockImeSession.create( 784 InstrumentationRegistry.getInstrumentation().getContext(), 785 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 786 new ImeSettings.Builder())) { 787 final ImeEventStream stream = imeSession.openEventStream(); 788 789 final String marker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 790 final EditText editText = launchTestActivity(marker); 791 792 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 793 notExpectEvent( 794 stream, 795 editorMatcher("onStartInputView", marker), 796 NOT_EXPECT_TIMEOUT); 797 798 addVirtualStylusIdForTestSession(); 799 injectStylusEventToEditorAndVerify(editText, stream, imeSession, marker, 800 true /* verifyHandwritingStart */, 801 false /* verifyHandwritingWindowShown */, 802 false /* verifyHandwritingWindowNotShown */); 803 804 // Inject finger swipe up on navbar. 805 TestUtils.injectNavBarToHomeGestureEvents( 806 ((Activity) editText.getContext()), MotionEvent.TOOL_TYPE_FINGER); 807 808 // Handwriting is finished if navigation gesture is executed. 809 // Make sure handwriting is finished to ensure swipe to home works. 810 expectEvent( 811 stream, 812 editorMatcher("onFinishStylusHandwriting", marker), 813 // BlastSyncEngine has a 5s timeout when launcher fails to sync its 814 // transaction, exceeding it avoids flakes when that happens. 815 TIMEOUT_6_S); 816 } 817 } 818 819 @Test 820 /** 821 * Inject stylus events to a focused EditText that disables autoHandwriting. 822 * {@link InputMethodManager#startStylusHandwriting(View)} should not be called. 823 */ testAutoHandwritingDisabled()824 public void testAutoHandwritingDisabled() throws Exception { 825 try (MockImeSession imeSession = MockImeSession.create( 826 InstrumentationRegistry.getInstrumentation().getContext(), 827 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 828 new ImeSettings.Builder())) { 829 final ImeEventStream stream = imeSession.openEventStream(); 830 831 final String marker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 832 final EditText editText = launchTestActivity(marker); 833 editText.setAutoHandwritingEnabled(false); 834 835 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 836 notExpectEvent( 837 stream, 838 editorMatcher("onStartInputView", marker), 839 NOT_EXPECT_TIMEOUT); 840 841 addVirtualStylusIdForTestSession(); 842 TestUtils.injectStylusEvents(editText); 843 844 // TODO(215439842): check that keyboard is not shown. 845 // Handwriting should not start 846 notExpectEvent( 847 stream, 848 editorMatcher("onStartStylusHandwriting", marker), 849 NOT_EXPECT_TIMEOUT); 850 851 verifyStylusHandwritingWindowIsNotShown(stream, imeSession); 852 } 853 } 854 855 /** 856 * Set the default value of EditText's autoHandwritingEnabled to false. After it gains focus, 857 * set it to true and then inject stylus events, expecting handwriting to work correctly. 858 */ 859 @Test 860 @RequiresFlagsEnabled(FLAG_HANDWRITING_TRACK_DISABLED) testAutoHandwritingDisabledAndEnable()861 public void testAutoHandwritingDisabledAndEnable() throws Exception { 862 try (MockImeSession imeSession = MockImeSession.create( 863 InstrumentationRegistry.getInstrumentation().getContext(), 864 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 865 new ImeSettings.Builder())) { 866 final ImeEventStream stream = imeSession.openEventStream(); 867 final String marker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 868 869 final AtomicReference<EditText> editTextRef = new AtomicReference<>(); 870 TestActivity.startSync(activity -> { 871 final LinearLayout layout = new LinearLayout(activity); 872 layout.setOrientation(LinearLayout.VERTICAL); 873 final EditText editText = new EditText(activity); 874 editTextRef.set(editText); 875 // Make auto handwriting enabled to false 876 editText.setAutoHandwritingEnabled(false); 877 layout.addView(editText); 878 editText.setHint("focused editText"); 879 editText.setPrivateImeOptions(marker); 880 editText.requestFocus(); 881 return layout; 882 }); 883 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 884 notExpectEvent(stream, editorMatcher("onStartInputView", marker), 885 NOT_EXPECT_TIMEOUT); 886 887 EditText editText = editTextRef.get(); 888 TestUtils.waitOnMainUntil(() -> editText.hasWindowFocus() 889 && editText.isFocused(), TIMEOUT); 890 891 // Make auto handwriting enable to true 892 editText.setAutoHandwritingEnabled(true); 893 addVirtualStylusIdForTestSession(); 894 895 injectStylusEventToEditorAndVerify(editText, stream, imeSession, 896 marker, true /* verifyHandwritingStart */, 897 true /* verifyHandwritingWindowShown */, 898 false /* verifyHandwritingWindowNotShown */); 899 900 // Finish handwriting to remove test stylus id. 901 imeSession.callFinishStylusHandwriting(); 902 expectEvent( 903 stream, 904 editorMatcher("onFinishStylusHandwriting", marker), 905 TIMEOUT_1_S); 906 } 907 } 908 909 910 @Test 911 /** 912 * Inject stylus events out of a focused editor's view bound. 913 * {@link InputMethodManager#startStylusHandwriting(View)} should not be called for this editor. 914 */ testAutoHandwritingOutOfBound()915 public void testAutoHandwritingOutOfBound() throws Exception { 916 try (MockImeSession imeSession = MockImeSession.create( 917 InstrumentationRegistry.getInstrumentation().getContext(), 918 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 919 new ImeSettings.Builder())) { 920 final ImeEventStream stream = imeSession.openEventStream(); 921 922 final String marker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 923 final EditText editText = launchTestActivity(marker); 924 editText.setAutoHandwritingEnabled(false); 925 926 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 927 notExpectEvent( 928 stream, 929 editorMatcher("onStartInputView", marker), 930 NOT_EXPECT_TIMEOUT); 931 932 addVirtualStylusIdForTestSession(); 933 // Inject stylus events out of the editor boundary. 934 TestUtils.injectStylusEvents(editText, editText.getWidth() / 2, 935 -HANDWRITING_BOUNDS_OFFSET_PX - 50); 936 // keyboard shouldn't show up. 937 notExpectEvent( 938 stream, 939 editorMatcher("onStartInputView", marker), 940 NOT_EXPECT_TIMEOUT); 941 // Handwriting should not start 942 notExpectEvent( 943 stream, 944 editorMatcher("onStartStylusHandwriting", marker), 945 NOT_EXPECT_TIMEOUT); 946 947 verifyStylusHandwritingWindowIsNotShown(stream, imeSession); 948 } 949 } 950 951 @Test 952 /** 953 * Inject Stylus events on top of an unfocused editor and verify Handwriting is started and 954 * InkWindow is displayed. 955 */ testHandwriting_unfocusedEditText()956 public void testHandwriting_unfocusedEditText() throws Exception { 957 try (MockImeSession imeSession = MockImeSession.create( 958 InstrumentationRegistry.getInstrumentation().getContext(), 959 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 960 new ImeSettings.Builder())) { 961 final ImeEventStream stream = imeSession.openEventStream(); 962 963 final String focusedMarker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 964 final String unfocusedMarker = getTestMarker(NON_FOCUSED_EDIT_TEXT_TAG); 965 final Pair<EditText, EditText> editTextPair = 966 launchTestActivity(focusedMarker, unfocusedMarker); 967 final EditText unfocusedEditText = editTextPair.second; 968 969 expectEvent(stream, editorMatcher("onStartInput", focusedMarker), TIMEOUT); 970 notExpectEvent( 971 stream, 972 editorMatcher("onStartInputView", focusedMarker), 973 NOT_EXPECT_TIMEOUT); 974 975 addVirtualStylusIdForTestSession(); 976 final int touchSlop = getTouchSlop(); 977 final int startX = unfocusedEditText.getWidth() / 2; 978 final int startY = 2 * touchSlop; 979 // (endX, endY) is out of bound to avoid that unfocusedEditText is focused due to the 980 // stylus touch. 981 final int endX = startX; 982 final int endY = unfocusedEditText.getHeight() + 2 * touchSlop; 983 final int number = 5; 984 985 TestUtils.injectStylusDownEvent(unfocusedEditText, startX, startY); 986 TestUtils.injectStylusMoveEvents(unfocusedEditText, startX, startY, 987 endX, endY, number); 988 try { 989 // Handwriting should already be initiated before ACTION_UP. 990 // unfocusedEditor is focused and triggers onStartInput. 991 expectEvent(stream, editorMatcher("onStartInput", unfocusedMarker), TIMEOUT); 992 // keyboard shouldn't show up. 993 notExpectEvent( 994 stream, 995 editorMatcher("onStartInputView", unfocusedMarker), 996 NOT_EXPECT_TIMEOUT); 997 // Handwriting should start on the unfocused EditText. 998 expectEvent( 999 stream, 1000 editorMatcher("onStartStylusHandwriting", unfocusedMarker), 1001 TIMEOUT); 1002 verifyStylusHandwritingWindowIsShown(stream, imeSession); 1003 } finally { 1004 TestUtils.injectStylusUpEvent(unfocusedEditText, endX, endY); 1005 } 1006 } 1007 } 1008 1009 /** 1010 * Inject stylus events on top of an unfocused password EditText and verify keyboard is shown 1011 * and handwriting is not started 1012 */ 1013 @Test 1014 @RequiresFlagsEnabled(FLAG_HANDWRITING_UNSUPPORTED_MESSAGE) testHandwriting_unfocusedEditText_password()1015 public void testHandwriting_unfocusedEditText_password() throws Exception { 1016 try (MockImeSession imeSession = MockImeSession.create( 1017 InstrumentationRegistry.getInstrumentation().getContext(), 1018 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 1019 new ImeSettings.Builder())) { 1020 final ImeEventStream stream = imeSession.openEventStream(); 1021 1022 final String focusedMarker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 1023 final String unfocusedMarker = getTestMarker(NON_FOCUSED_EDIT_TEXT_TAG); 1024 final Pair<EditText, EditText> editTextPair = 1025 launchTestActivity(focusedMarker, unfocusedMarker); 1026 final EditText unfocusedEditText = editTextPair.second; 1027 unfocusedEditText.post(() -> unfocusedEditText.setInputType( 1028 InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD)); 1029 1030 expectEvent(stream, editorMatcher("onStartInput", focusedMarker), TIMEOUT); 1031 notExpectEvent( 1032 stream, 1033 editorMatcher("onStartInputView", focusedMarker), 1034 NOT_EXPECT_TIMEOUT); 1035 1036 addVirtualStylusIdForTestSession(); 1037 final int touchSlop = getTouchSlop(); 1038 final int startX = unfocusedEditText.getWidth() / 2; 1039 final int startY = 2 * touchSlop; 1040 // (endX, endY) is out of bound to avoid that unfocusedEditText is focused due to the 1041 // stylus touch. 1042 final int endX = startX; 1043 final int endY = unfocusedEditText.getHeight() + 2 * touchSlop; 1044 final int number = 5; 1045 1046 TestUtils.injectStylusDownEvent(unfocusedEditText, startX, startY); 1047 TestUtils.injectStylusMoveEvents(unfocusedEditText, startX, startY, 1048 endX, endY, number); 1049 1050 // Handwriting is not started since it is not supported for password fields, but it is 1051 // focused and the soft keyboard is shown. 1052 expectEvent(stream, editorMatcher("onStartInput", unfocusedMarker), TIMEOUT); 1053 expectEvent(stream, editorMatcher("onStartInputView", unfocusedMarker), TIMEOUT); 1054 notExpectEvent( 1055 stream, 1056 editorMatcher("onStartStylusHandwriting", unfocusedMarker), 1057 NOT_EXPECT_TIMEOUT); 1058 verifyStylusHandwritingWindowIsNotShown(stream, imeSession); 1059 1060 TestUtils.injectStylusUpEvent(unfocusedEditText, endX, endY); 1061 } 1062 } 1063 1064 /** 1065 * Inject stylus events on top of an unfocused password EditText with 1066 * setShowSoftInputOnFocus(false) and verify keyboard is not shown and handwriting is not 1067 * started. 1068 */ 1069 @Test 1070 @RequiresFlagsEnabled({FLAG_HANDWRITING_UNSUPPORTED_MESSAGE, 1071 FLAG_HANDWRITING_UNSUPPORTED_SHOW_SOFT_INPUT_FIX}) testHandwriting_unfocusedEditText_password_showSoftInputOnFocusFalse()1072 public void testHandwriting_unfocusedEditText_password_showSoftInputOnFocusFalse() 1073 throws Exception { 1074 try (MockImeSession imeSession = MockImeSession.create( 1075 InstrumentationRegistry.getInstrumentation().getContext(), 1076 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 1077 new ImeSettings.Builder())) { 1078 final ImeEventStream stream = imeSession.openEventStream(); 1079 1080 final String focusedMarker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 1081 final String unfocusedMarker = getTestMarker(NON_FOCUSED_EDIT_TEXT_TAG); 1082 final Pair<EditText, EditText> editTextPair = 1083 launchTestActivity(focusedMarker, unfocusedMarker); 1084 final EditText unfocusedEditText = editTextPair.second; 1085 unfocusedEditText.post(() -> { 1086 unfocusedEditText.setInputType( 1087 InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); 1088 unfocusedEditText.setShowSoftInputOnFocus(false); 1089 }); 1090 1091 expectEvent(stream, editorMatcher("onStartInput", focusedMarker), TIMEOUT); 1092 notExpectEvent( 1093 stream, 1094 editorMatcher("onStartInputView", focusedMarker), 1095 NOT_EXPECT_TIMEOUT); 1096 1097 addVirtualStylusIdForTestSession(); 1098 final int touchSlop = getTouchSlop(); 1099 final int startX = unfocusedEditText.getWidth() / 2; 1100 final int startY = 2 * touchSlop; 1101 // (endX, endY) is out of bound to avoid that unfocusedEditText is focused due to the 1102 // stylus touch. 1103 final int endX = startX; 1104 final int endY = unfocusedEditText.getHeight() + 2 * touchSlop; 1105 final int number = 5; 1106 1107 TestUtils.injectStylusDownEvent(unfocusedEditText, startX, startY); 1108 TestUtils.injectStylusMoveEvents(unfocusedEditText, startX, startY, 1109 endX, endY, number); 1110 1111 // Handwriting is not started since it is not supported for password fields, but it is 1112 // focused. The soft keyboard is not shown since getShowSoftInputOnFocus() is false. 1113 expectEvent(stream, editorMatcher("onStartInput", unfocusedMarker), TIMEOUT); 1114 notExpectEvent(stream, editorMatcher("onStartInputView", unfocusedMarker), 1115 NOT_EXPECT_TIMEOUT); 1116 notExpectEvent( 1117 stream, 1118 editorMatcher("onStartStylusHandwriting", unfocusedMarker), 1119 NOT_EXPECT_TIMEOUT); 1120 verifyStylusHandwritingWindowIsNotShown(stream, imeSession); 1121 1122 TestUtils.injectStylusUpEvent(unfocusedEditText, endX, endY); 1123 } 1124 } 1125 1126 /** 1127 * With handwriting setting disabled, inject stylus events on top of an unfocused EditText and 1128 * verify handwriting is not started and keyboard is not shown. 1129 */ 1130 @Test 1131 @RequiresFlagsEnabled(FLAG_HANDWRITING_UNSUPPORTED_MESSAGE) testHandwriting_unfocusedEditText_prefDisabled()1132 public void testHandwriting_unfocusedEditText_prefDisabled() throws Exception { 1133 try (MockImeSession imeSession = MockImeSession.create( 1134 InstrumentationRegistry.getInstrumentation().getContext(), 1135 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 1136 new ImeSettings.Builder())) { 1137 final ImeEventStream stream = imeSession.openEventStream(); 1138 1139 // Disable preference 1140 SystemUtil.runWithShellPermissionIdentity(() -> { 1141 Settings.Secure.putInt(mContext.getContentResolver(), 1142 STYLUS_HANDWRITING_ENABLED, SETTING_VALUE_OFF); 1143 }, Manifest.permission.WRITE_SECURE_SETTINGS); 1144 mShouldRestoreInitialHwState = true; 1145 1146 final String focusedMarker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 1147 final String unfocusedMarker = getTestMarker(NON_FOCUSED_EDIT_TEXT_TAG); 1148 final Pair<EditText, EditText> editTextPair = 1149 launchTestActivity(focusedMarker, unfocusedMarker); 1150 final EditText unfocusedEditText = editTextPair.second; 1151 1152 expectEvent(stream, editorMatcher("onStartInput", focusedMarker), TIMEOUT); 1153 notExpectEvent( 1154 stream, 1155 editorMatcher("onStartInputView", focusedMarker), 1156 NOT_EXPECT_TIMEOUT); 1157 1158 addVirtualStylusIdForTestSession(); 1159 final int touchSlop = getTouchSlop(); 1160 final int x = unfocusedEditText.getWidth() / 2; 1161 final int startY = 2 * touchSlop; 1162 // (endX, endY) is out of bound to avoid that unfocusedEditText is focused due to the 1163 // stylus touch. 1164 final int endY = unfocusedEditText.getHeight() + 2 * touchSlop; 1165 final int number = 5; 1166 1167 TestUtils.injectStylusDownEvent(unfocusedEditText, x, startY); 1168 TestUtils.injectStylusMoveEvents(unfocusedEditText, x, startY, x, endY, number); 1169 1170 // Handwriting is not started, 1171 notExpectEvent(stream, editorMatcher("onStartInput", unfocusedMarker), 1172 NOT_EXPECT_TIMEOUT); 1173 notExpectEvent(stream, editorMatcher("onStartInputView", unfocusedMarker), 1174 NOT_EXPECT_TIMEOUT); 1175 notExpectEvent( 1176 stream, 1177 editorMatcher("onStartStylusHandwriting", unfocusedMarker), 1178 NOT_EXPECT_TIMEOUT); 1179 verifyStylusHandwritingWindowIsNotShown(stream, imeSession); 1180 1181 TestUtils.injectStylusUpEvent(unfocusedEditText, x, endY); 1182 } 1183 } 1184 1185 /** 1186 * Inject stylus events on top of an unfocused editor which disabled the autoHandwriting and 1187 * verify keyboard is shown and handwriting is not started. 1188 */ 1189 @Test 1190 @RequiresFlagsEnabled(FLAG_HANDWRITING_UNSUPPORTED_MESSAGE) testHandwriting_unfocusedEditText_autoHandwritingDisabled()1191 public void testHandwriting_unfocusedEditText_autoHandwritingDisabled() throws Exception { 1192 try (MockImeSession imeSession = MockImeSession.create( 1193 InstrumentationRegistry.getInstrumentation().getContext(), 1194 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 1195 new ImeSettings.Builder())) { 1196 final ImeEventStream stream = imeSession.openEventStream(); 1197 1198 final String focusedMarker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 1199 final String unfocusedMarker = getTestMarker(NON_FOCUSED_EDIT_TEXT_TAG); 1200 final Pair<EditText, EditText> editTextPair = 1201 launchTestActivity(focusedMarker, unfocusedMarker); 1202 final EditText unfocusedEditText = editTextPair.second; 1203 unfocusedEditText.setAutoHandwritingEnabled(false); 1204 1205 expectEvent(stream, editorMatcher("onStartInput", focusedMarker), TIMEOUT); 1206 notExpectEvent( 1207 stream, 1208 editorMatcher("onStartInputView", focusedMarker), 1209 NOT_EXPECT_TIMEOUT); 1210 1211 addVirtualStylusIdForTestSession(); 1212 final int touchSlop = getTouchSlop(); 1213 final int startX = unfocusedEditText.getWidth() / 2; 1214 final int startY = 2 * touchSlop; 1215 // (endX, endY) is out of bound to avoid that unfocusedEditText is focused due to the 1216 // stylus touch. 1217 final int endX = startX; 1218 final int endY = -2 * touchSlop; 1219 final int number = 5; 1220 TestUtils.injectStylusDownEvent(unfocusedEditText, startX, startY); 1221 TestUtils.injectStylusMoveEvents(unfocusedEditText, startX, startY, 1222 endX, endY, number); 1223 TestUtils.injectStylusUpEvent(unfocusedEditText, endX, endY); 1224 1225 // Handwriting is not started since it is disabled for the EditText, but it is focused 1226 // and the soft keyboard is shown. 1227 expectEvent(stream, editorMatcher("onStartInput", unfocusedMarker), TIMEOUT); 1228 expectEvent(stream, editorMatcher("onStartInputView", unfocusedMarker), TIMEOUT); 1229 notExpectEvent( 1230 stream, 1231 editorMatcher("onStartStylusHandwriting", unfocusedMarker), 1232 NOT_EXPECT_TIMEOUT); 1233 1234 verifyStylusHandwritingWindowIsNotShown(stream, imeSession); 1235 } 1236 } 1237 1238 /** 1239 * Inject finger taps during ongoing stylus handwriting and make sure those taps are ignored 1240 * until stylus ACTION_UP. 1241 */ 1242 @Test 1243 @RequiresFlagsEnabled(FLAG_DEVICE_ASSOCIATIONS) testHandwriting_fingerTouchIsIgnored()1244 public void testHandwriting_fingerTouchIsIgnored() throws Exception { 1245 Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 1246 try (MockImeSession imeSession = MockImeSession.create( 1247 instrumentation.getContext(), 1248 instrumentation.getUiAutomation(), 1249 new ImeSettings.Builder())) { 1250 final ImeEventStream stream = imeSession.openEventStream(); 1251 1252 final String focusedMarker = getTestMarker(); 1253 final String unfocusedMarker = getTestMarker(); 1254 final Pair<EditText, EditText> editTextPair = 1255 launchTestActivity(focusedMarker, unfocusedMarker); 1256 final EditText focusedEditText = editTextPair.first; 1257 final EditText unfocusedEditText = editTextPair.second; 1258 Context context = focusedEditText.getContext(); 1259 1260 final Display display = context.getDisplay(); 1261 try (UinputTouchDevice touch = new UinputTouchScreen(instrumentation, display); 1262 UinputTouchDevice stylus = new UinputStylus(instrumentation, display)) { 1263 1264 expectEvent(stream, editorMatcher("onStartInput", focusedMarker), TIMEOUT); 1265 notExpectEvent( 1266 stream, 1267 editorMatcher("onStartInputView", focusedMarker), 1268 NOT_EXPECT_TIMEOUT); 1269 1270 addVirtualStylusIdForTestSession(); 1271 final int touchSlop = getTouchSlop(); 1272 int startX = focusedEditText.getWidth() / 2; 1273 final int startY = 2 * touchSlop; 1274 int endX = startX; 1275 int endY = focusedEditText.getHeight() + 2 * touchSlop; 1276 final int number = 5; 1277 1278 // set a longer idle-timeout for handwriting session. 1279 assertTrue(expectCommand( 1280 stream, imeSession.callSetStylusHandwritingTimeout(TIMEOUT * 2), 1281 TIMEOUT).getReturnBooleanValue()); 1282 UinputTouchDevice.Pointer stylusPointer = TestUtils.injectStylusDownEvent(stylus, 1283 focusedEditText, startX, startY); 1284 TestUtils.injectStylusMoveEvents(stylusPointer, focusedEditText, startX, startY, 1285 endX, endY, number); 1286 1287 // Handwriting should start on the focused EditText. 1288 expectEvent( 1289 stream, 1290 editorMatcher("onStartStylusHandwriting", focusedMarker), 1291 TIMEOUT); 1292 1293 // Set IME stylus Ink view to listen for ACTION_UP MotionEvent 1294 assertTrue(expectCommand( 1295 stream, 1296 imeSession.callSetStylusHandwritingInkView(), 1297 TIMEOUT).getReturnBooleanValue()); 1298 1299 TestUtils.injectStylusUpEvent(stylusPointer); 1300 waitForStylusAction(MotionEvent.ACTION_UP, stream, imeSession, endX, endY); 1301 1302 // Finger tap on unfocused editor. 1303 TestUtils.injectFingerClickOnViewCenter(touch, unfocusedEditText); 1304 1305 // Finger tap should passthrough and unfocused editor should steal focus. 1306 TestUtils.waitOnMainUntil(unfocusedEditText::hasFocus, 1307 TIMEOUT, "unfocusedEditText should gain focus on finger tap"); 1308 1309 // reset focus back to focusedEditText. 1310 focusedEditText.post(focusedEditText::requestFocus); 1311 1312 // Inject a lot of stylus events async. 1313 stylusPointer = TestUtils.injectStylusDownEvent(stylus, focusedEditText, startX, 1314 startY); 1315 TestUtils.injectStylusMoveEvents(stylusPointer, focusedEditText, startX, startY, 1316 endX, endY, number); 1317 // Set IME stylus Ink view to listen for ACTION_MOVE MotionEvents. 1318 // (This can only be set on Handwriting window, which exists for the duration of 1319 // session). 1320 assertTrue(expectCommand( 1321 stream, 1322 imeSession.callSetStylusHandwritingInkView(), 1323 TIMEOUT).getReturnBooleanValue()); 1324 // After handwriting has started, inject another ACTION_MOVE so we receive that on 1325 // InkView. 1326 TestUtils.injectStylusMoveEvents(stylusPointer, focusedEditText, endX, endY, 1327 endX, endY, number); 1328 waitForStylusAction(MotionEvent.ACTION_MOVE, stream, imeSession, endX, endY); 1329 1330 // Finger tap on unfocused editor while stylus is still injecting events. 1331 TestUtils.injectFingerClickOnViewCenter(touch, unfocusedEditText); 1332 // Finger tap should be ignored and unfocused editor shouldn't steal focus. 1333 TestUtils.waitOnMainUntil(() -> !unfocusedEditText.hasFocus(), 1334 TIMEOUT_1_S, "Finger tap on unfocusedEditText should be ignored"); 1335 TestUtils.injectStylusUpEvent(stylusPointer); 1336 1337 notExpectEvent( 1338 stream, 1339 editorMatcher("finishStylusHandwriting", unfocusedMarker), 1340 NOT_EXPECT_TIMEOUT); 1341 } finally { 1342 imeSession.callFinishStylusHandwriting(); 1343 } 1344 } 1345 } 1346 1347 // wait for stylus action to be delivered to IME. waitForStylusAction( int action, ImeEventStream stream, MockImeSession imeSession, int x, int y)1348 private void waitForStylusAction( 1349 int action, ImeEventStream stream, MockImeSession imeSession, int x, int y) 1350 throws TimeoutException { 1351 long elapsedMs = 0; 1352 int sleepDurationMs = 50; 1353 while (elapsedMs < TIMEOUT_1_S) { 1354 final ArrayList<MotionEvent> capturedBatchedEvents = 1355 expectCommand(stream, imeSession.callGetStylusHandwritingEvents(), TIMEOUT) 1356 .getReturnParcelableArrayListValue(); 1357 assertNotNull(capturedBatchedEvents); 1358 assertFalse("captured events shouldn't be empty", capturedBatchedEvents.isEmpty()); 1359 1360 MotionEvent lastEvent = capturedBatchedEvents.get(capturedBatchedEvents.size() - 1); 1361 for (MotionEvent event : capturedBatchedEvents) { 1362 if (lastEvent.getAction() == action && event.getX() == x && event.getY() == y) { 1363 break; 1364 } 1365 } 1366 1367 elapsedMs += sleepDurationMs; 1368 try { 1369 Thread.sleep(sleepDurationMs); 1370 } catch (InterruptedException e) { 1371 throw new RuntimeException(e); 1372 } 1373 } 1374 } 1375 waitUntilActivityReadyForInput(Activity activity)1376 private void waitUntilActivityReadyForInput(Activity activity) { 1377 // If we requested an orientation change, just waiting for the window to be visible is not 1378 // sufficient. We should first wait for the transitions to stop, and the for app's UI thread 1379 // to process them before making sure the window is visible. 1380 try { 1381 TestUtils.waitUntilActivityReadyForInputInjection( 1382 activity, StylusHandwritingTest.this.getClass().getName(), 1383 "test: " + StylusHandwritingTest.this.mTestName.getMethodName() 1384 + ", virtualDisplayId=" + activity.getDisplayId() 1385 ); 1386 } catch (InterruptedException e) { 1387 throw new RuntimeException(e); 1388 } 1389 } 1390 1391 /** 1392 * Inject stylus top on an editor and verify stylus source is detected with 1393 * {@link InputMethodService#onUpdateEditorToolType(int)} lifecycle method. 1394 */ 1395 @Test 1396 @FlakyTest 1397 @RequiresFlagsEnabled(FLAG_DEVICE_ASSOCIATIONS) testOnViewClicked_withStylusTap()1398 public void testOnViewClicked_withStylusTap() throws Exception { 1399 try (MockImeSession imeSession = MockImeSession.create( 1400 InstrumentationRegistry.getInstrumentation().getContext(), 1401 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 1402 new ImeSettings.Builder())) { 1403 final ImeEventStream stream = imeSession.openEventStream(); 1404 1405 final String focusedMarker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 1406 final String unfocusedMarker = getTestMarker(NON_FOCUSED_EDIT_TEXT_TAG); 1407 final Pair<EditText, EditText> pair = 1408 launchTestActivity(focusedMarker, unfocusedMarker); 1409 final EditText focusedEditText = pair.first; 1410 final EditText unfocusedEditText = pair.second; 1411 1412 int x = focusedEditText.getWidth() / 2; 1413 int y = focusedEditText.getHeight() / 2; 1414 1415 // Tap with stylus on focused editor 1416 try (UinputTouchDevice stylus = 1417 new UinputStylus( 1418 InstrumentationRegistry.getInstrumentation(), 1419 focusedEditText.getDisplay())) { 1420 expectEvent(stream, editorMatcher("onStartInput", focusedMarker), TIMEOUT); 1421 notExpectEvent( 1422 stream, 1423 editorMatcher("onStartInputView", focusedMarker), 1424 NOT_EXPECT_TIMEOUT); 1425 1426 addVirtualStylusIdForTestSession(); 1427 1428 UinputTouchDevice.Pointer stylusPointer = 1429 TestUtils.injectStylusDownEvent(stylus, focusedEditText, x, y); 1430 TestUtils.injectStylusUpEvent(stylusPointer); 1431 1432 int toolType = MotionEvent.TOOL_TYPE_STYLUS; 1433 1434 expectEvent(stream, onUpdateEditorToolTypeMatcher(toolType), TIMEOUT); 1435 1436 // Tap with stylus on unfocused editor 1437 x = unfocusedEditText.getWidth() / 2; 1438 y = unfocusedEditText.getHeight() / 2; 1439 stylusPointer = TestUtils.injectStylusDownEvent(stylus, unfocusedEditText, x, y); 1440 TestUtils.injectStylusUpEvent(stylusPointer); 1441 if (mFlagsValueProvider.getBoolean(FLAG_USE_HANDWRITING_LISTENER_FOR_TOOLTYPE)) { 1442 expectEvent( 1443 stream, 1444 startInputInitialEditorToolMatcher(toolType, unfocusedMarker), 1445 TIMEOUT); 1446 } else { 1447 expectEvent(stream, onStartInputMatcher(toolType, unfocusedMarker), TIMEOUT); 1448 } 1449 } 1450 } 1451 } 1452 1453 /** 1454 * Inject finger top on an editor and verify stylus source is detected with 1455 * {@link InputMethodService#onUpdateEditorToolType(int)} lifecycle method. 1456 */ 1457 @Test 1458 @FlakyTest 1459 @RequiresFlagsEnabled(FLAG_DEVICE_ASSOCIATIONS) testOnViewClicked_withFingerTap()1460 public void testOnViewClicked_withFingerTap() throws Exception { 1461 try (MockImeSession imeSession = MockImeSession.create( 1462 InstrumentationRegistry.getInstrumentation().getContext(), 1463 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 1464 new ImeSettings.Builder())) { 1465 final ImeEventStream stream = imeSession.openEventStream(); 1466 1467 final String focusedMarker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 1468 final String unfocusedMarker = getTestMarker(NON_FOCUSED_EDIT_TEXT_TAG); 1469 final Pair<EditText, EditText> pair = 1470 launchTestActivity(focusedMarker, unfocusedMarker); 1471 final EditText focusedEditText = pair.first; 1472 final EditText unfocusedEditText = pair.second; 1473 1474 try (UinputTouchDevice touch = 1475 new UinputTouchScreen( 1476 InstrumentationRegistry.getInstrumentation(), 1477 unfocusedEditText.getDisplay())) { 1478 int toolTypeFinger = MotionEvent.TOOL_TYPE_FINGER; 1479 expectEvent(stream, editorMatcher("onStartInput", focusedMarker), TIMEOUT); 1480 notExpectEvent( 1481 stream, 1482 editorMatcher("onStartInputView", focusedMarker), 1483 NOT_EXPECT_TIMEOUT); 1484 TestUtils.injectFingerClickOnViewCenter(touch, focusedEditText); 1485 1486 expectEvent( 1487 stream, 1488 onUpdateEditorToolTypeMatcher(MotionEvent.TOOL_TYPE_FINGER), 1489 TIMEOUT); 1490 1491 // tap on unfocused editor 1492 TestUtils.injectFingerClickOnViewCenter(touch, unfocusedEditText); 1493 expectEvent(stream, onStartInputMatcher(toolTypeFinger, unfocusedMarker), TIMEOUT); 1494 expectEvent( 1495 stream, 1496 onUpdateEditorToolTypeMatcher(MotionEvent.TOOL_TYPE_FINGER), 1497 TIMEOUT); 1498 } 1499 } 1500 } 1501 1502 /** 1503 * Inject stylus handwriting event on an editor and verify stylus source is detected with {@link 1504 * InputMethodService#onUpdateEditorToolType(int)} on next startInput(). 1505 */ 1506 @Test 1507 @FlakyTest 1508 @DebugInputRule.DebugInput(bug = 380535703) 1509 @RequiresFlagsEnabled(FLAG_DEVICE_ASSOCIATIONS) testOnViewClicked_withStylusHandwriting()1510 public void testOnViewClicked_withStylusHandwriting() throws Exception { 1511 Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 1512 try (MockImeSession imeSession = MockImeSession.create( 1513 InstrumentationRegistry.getInstrumentation().getContext(), 1514 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 1515 new ImeSettings.Builder())) { 1516 final ImeEventStream stream = imeSession.openEventStream(); 1517 1518 addVirtualStylusIdForTestSession(); 1519 1520 final String focusedMarker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 1521 final String unfocusedMarker = getTestMarker(NON_FOCUSED_EDIT_TEXT_TAG); 1522 final Pair<EditText, EditText> pair = 1523 launchTestActivity(focusedMarker, unfocusedMarker); 1524 final EditText focusedEditText = pair.first; 1525 final EditText unfocusedEditText = pair.second; 1526 1527 expectEvent(stream, editorMatcher("onStartInput", focusedMarker), TIMEOUT); 1528 notExpectEvent( 1529 stream, 1530 editorMatcher("onStartInputView", focusedMarker), 1531 NOT_EXPECT_TIMEOUT); 1532 1533 Context context = focusedEditText.getContext(); 1534 final Display display = context.getDisplay(); 1535 1536 try (UinputTouchDevice touch = new UinputTouchScreen(instrumentation, display); 1537 UinputTouchDevice stylus = new UinputStylus(instrumentation, display)) { 1538 // Finger tap on editor and verify onUpdateEditorToolType 1539 // Finger tap on unfocused editor. 1540 TestUtils.injectFingerClickOnViewCenter(touch, unfocusedEditText); 1541 int toolTypeFinger = 1542 MotionEvent.TOOL_TYPE_FINGER; 1543 expectEvent( 1544 stream, 1545 onUpdateEditorToolTypeMatcher(toolTypeFinger), 1546 TIMEOUT); 1547 1548 // Start handwriting on same focused editor 1549 final int touchSlop = getTouchSlop(); 1550 int startX = focusedEditText.getWidth() / 2; 1551 int startY = focusedEditText.getHeight() / 2; 1552 int endX = startX + 2 * touchSlop; 1553 int endY = startY + 2 * touchSlop; 1554 final int number = 5; 1555 UinputTouchDevice.Pointer stylusPointer = 1556 TestUtils.injectStylusDownEvent(stylus, focusedEditText, startX, startY); 1557 TestUtils.injectStylusMoveEvents(stylusPointer, focusedEditText, startX, startY, 1558 endX, endY, number); 1559 try { 1560 // Handwriting should start. 1561 expectEvent( 1562 stream, 1563 editorMatcher("onStartStylusHandwriting", focusedMarker), 1564 TIMEOUT); 1565 } finally { 1566 TestUtils.injectStylusUpEvent(stylusPointer); 1567 } 1568 imeSession.callFinishStylusHandwriting(); 1569 expectEvent( 1570 stream, 1571 editorMatcher("onFinishStylusHandwriting", focusedMarker), 1572 TIMEOUT_1_S); 1573 1574 addVirtualStylusIdForTestSession(); 1575 // Now start handwriting on unfocused editor and verify toolType is available in 1576 // EditorInfo 1577 startX = unfocusedEditText.getWidth() / 2; 1578 startY = unfocusedEditText.getHeight() / 2; 1579 endX = startX + 2 * touchSlop; 1580 endY = startY + 2 * touchSlop; 1581 stylusPointer = 1582 TestUtils.injectStylusDownEvent(stylus, unfocusedEditText, startX, startY); 1583 TestUtils.injectStylusMoveEvents(stylusPointer, unfocusedEditText, startX, startY, 1584 endX, endY, number); 1585 try { 1586 expectEvent(stream, editorMatcher("onStartInput", unfocusedMarker), TIMEOUT); 1587 1588 // toolType should be updated on next stylus handwriting start 1589 expectEvent(stream, onStartStylusHandwritingMatcher( 1590 MotionEvent.TOOL_TYPE_STYLUS, unfocusedMarker), TIMEOUT); 1591 } finally { 1592 TestUtils.injectStylusUpEvent(stylusPointer); 1593 } 1594 } 1595 } 1596 } 1597 1598 /** 1599 * Inject KeyEvent and Stylus tap verify toolType is detected with 1600 * {@link InputMethodService#onUpdateEditorToolType(int)} lifecycle method. 1601 */ 1602 @RequiresFlagsEnabled(FLAG_USE_HANDWRITING_LISTENER_FOR_TOOLTYPE) 1603 @Test testOnViewClicked_withKeyEvent()1604 public void testOnViewClicked_withKeyEvent() throws Exception { 1605 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 1606 try (MockImeSession imeSession = MockImeSession.create( 1607 instrumentation.getContext(), instrumentation.getUiAutomation(), 1608 new ImeSettings.Builder())) { 1609 final ImeEventStream stream = imeSession.openEventStream(); 1610 1611 final String focusedMarker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 1612 final String unfocusedMarker = getTestMarker(NON_FOCUSED_EDIT_TEXT_TAG); 1613 launchTestActivityNoEditorFocus(focusedMarker, unfocusedMarker); 1614 1615 // Send any KeyEvent when editor isn't focused. 1616 instrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_0); 1617 1618 // KeyEvents are identified as unknown tooltype. 1619 int toolType = MotionEvent.TOOL_TYPE_UNKNOWN; 1620 expectEvent( 1621 stream, 1622 onUpdateEditorToolTypeMatcher(toolType), 1623 TIMEOUT); 1624 } 1625 } 1626 onStartInputMatcher(int toolType, String marker)1627 private static DescribedPredicate<ImeEvent> onStartInputMatcher(int toolType, String marker) { 1628 Predicate<ImeEvent> matcher = event -> { 1629 if (!TextUtils.equals("onStartInput", event.getEventName())) { 1630 return false; 1631 } 1632 EditorInfo info = event.getArguments().getParcelable("editorInfo"); 1633 return info.getInitialToolType() == toolType 1634 && TextUtils.equals(marker, info.privateImeOptions); 1635 }; 1636 return withDescription( 1637 "onStartInput(initialToolType=" + toolType + ",marker=" + marker + ")", matcher); 1638 } 1639 1640 startInputInitialEditorToolMatcher( int expectedToolType, @NonNull String marker)1641 private static DescribedPredicate<ImeEvent> startInputInitialEditorToolMatcher( 1642 int expectedToolType, @NonNull String marker) { 1643 return withDescription("onStartInput()" + "(marker=" + marker + ")", event -> { 1644 if (!TextUtils.equals("onStartInput", event.getEventName())) { 1645 return false; 1646 } 1647 final EditorInfo editorInfo = event.getArguments().getParcelable("editorInfo"); 1648 return expectedToolType == editorInfo.getInitialToolType(); 1649 }); 1650 } 1651 1652 private static DescribedPredicate<ImeEvent> onStartStylusHandwritingMatcher( 1653 int toolType, String marker) { 1654 Predicate<ImeEvent> matcher = event -> { 1655 if (!TextUtils.equals("onStartStylusHandwriting", event.getEventName())) { 1656 return false; 1657 } 1658 EditorInfo info = event.getArguments().getParcelable("editorInfo"); 1659 return info.getInitialToolType() == toolType 1660 && TextUtils.equals(marker, info.privateImeOptions); 1661 }; 1662 return withDescription( 1663 "onStartStylusHandwriting(initialToolType=" + toolType 1664 + ", marker=" + marker + ")", matcher); 1665 } 1666 1667 private static DescribedPredicate<ImeEvent> onUpdateEditorToolTypeMatcher(int expectedToolType) { 1668 Predicate<ImeEvent> matcher = event -> { 1669 if (!TextUtils.equals("onUpdateEditorToolType", event.getEventName())) { 1670 return false; 1671 } 1672 final int actualToolType = event.getArguments().getInt("toolType"); 1673 return actualToolType == expectedToolType; 1674 }; 1675 return withDescription("onUpdateEditorToolType(toolType=" + expectedToolType + ")", 1676 matcher); 1677 } 1678 1679 /** 1680 * Inject stylus events on top of a focused custom editor and verify handwriting is started and 1681 * stylus handwriting window is displayed. 1682 */ 1683 @Test 1684 public void testHandwriting_focusedCustomEditor() throws Exception { 1685 try (MockImeSession imeSession = MockImeSession.create( 1686 InstrumentationRegistry.getInstrumentation().getContext(), 1687 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 1688 new ImeSettings.Builder())) { 1689 final ImeEventStream stream = imeSession.openEventStream(); 1690 1691 final String focusedMarker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 1692 final String unfocusedMarker = getTestMarker(NON_FOCUSED_EDIT_TEXT_TAG); 1693 final Pair<CustomEditorView, CustomEditorView> customEditorPair = 1694 launchTestActivityWithCustomEditors(focusedMarker, unfocusedMarker); 1695 final CustomEditorView focusedCustomEditor = customEditorPair.first; 1696 1697 expectEvent(stream, editorMatcher("onStartInput", focusedMarker), TIMEOUT); 1698 notExpectEvent( 1699 stream, 1700 editorMatcher("onStartInputView", focusedMarker), 1701 NOT_EXPECT_TIMEOUT); 1702 1703 addVirtualStylusIdForTestSession(); 1704 1705 injectStylusEventToEditorAndVerify(focusedCustomEditor, stream, imeSession, 1706 focusedMarker, true /* verifyHandwritingStart */, 1707 true /* verifyHandwritingWindowShown */, 1708 false /* verifyHandwritingWindowNotShown */); 1709 1710 // Verify that stylus move events are swallowed by the handwriting initiator once 1711 // handwriting has been initiated and not dispatched to the view tree. 1712 assertThat(focusedCustomEditor.mStylusMoveEventCount) 1713 .isLessThan(NUMBER_OF_INJECTED_EVENTS); 1714 } 1715 } 1716 1717 /** 1718 * Inject stylus events on top of a handwriting initiation delegate view and verify handwriting 1719 * is started on the delegator editor and stylus handwriting window is displayed. 1720 */ 1721 @ApiTest(apis = { 1722 "android.view.inputmethod.InputMethodManager#acceptStylusHandwritingDelegation", 1723 "android.view.inputmethod.InputMethodManager#acceptStylusHandwritingDelegationAsync", 1724 "android.view.inputmethod.InputMethodManager#prepareStylusHandwritingDelegation", 1725 "android.view.View#setHandwritingDelegatorCallback", 1726 "android.view.View#setIsHandwritingDelegate"}) 1727 @Test 1728 public void testHandwriting_delegate() throws Exception { 1729 try (MockImeSession imeSession = MockImeSession.create( 1730 InstrumentationRegistry.getInstrumentation().getContext(), 1731 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 1732 new ImeSettings.Builder())) { 1733 final ImeEventStream stream = imeSession.openEventStream(); 1734 1735 final String editTextMarker = getTestMarker(); 1736 final View delegateView = 1737 launchTestActivityWithDelegate( 1738 editTextMarker, null /* delegateLatch */, 0 /* delegateDelayMs */); 1739 expectBindInput(stream, Process.myPid(), TIMEOUT); 1740 addVirtualStylusIdForTestSession(); 1741 1742 // After injecting DOWN and MOVE events, the handwriting initiator should trigger the 1743 // delegate view's callback which creates the EditText and requests focus, which should 1744 // then initiate handwriting for the EditText. 1745 injectStylusEventToEditorAndVerify(delegateView, stream, imeSession, 1746 editTextMarker, true /* verifyHandwritingStart */, 1747 true /* verifyHandwritingWindowShown */, 1748 false /* verifyHandwritingWindowNotShown */); 1749 } 1750 } 1751 1752 /** 1753 * When the IME supports connectionless handwriting sessions, inject stylus events on top of a 1754 * handwriting initiation delegator view and verify a connectionless handwriting session is 1755 * started. When the session is finished, verify that the delegation transition os triggered 1756 * and the recognised text is committed. 1757 */ 1758 @Test 1759 @RequiresFlagsEnabled(FLAG_CONNECTIONLESS_HANDWRITING) 1760 @ApiTest(apis = { 1761 "android.view.inputmethod.InputMethodManager" 1762 + "#startConnectionlessStylusHandwritingForDelegation", 1763 "android.view.inputmethod.InputMethodManager#acceptStylusHandwritingDelegation", 1764 "android.view.inputmethod.InputMethodService#onStartConnectionlessStylusHandwriting", 1765 "android.view.inputmethod.InputMethodService#finishConnectionlessStylusHandwriting"}) 1766 @FlakyTest(bugId = 329267066) 1767 public void testHandwriting_delegate_connectionless() throws Exception { 1768 try (MockImeSession imeSession = MockImeSession.create( 1769 InstrumentationRegistry.getInstrumentation().getContext(), 1770 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 1771 new ImeSettings.Builder().setConnectionlessHandwritingEnabled(true))) { 1772 final ImeEventStream stream = imeSession.openEventStream(); 1773 1774 final String delegateMarker = getTestMarker(); 1775 final View delegatorView = 1776 launchTestActivityWithDelegate( 1777 delegateMarker, null /* delegateLatch */, 0 /* delegateDelayMs */); 1778 expectBindInput(stream, Process.myPid(), TIMEOUT); 1779 addVirtualStylusIdForTestSession(); 1780 1781 int touchSlop = getTouchSlop(); 1782 int startX = delegatorView.getWidth() / 2; 1783 int startY = delegatorView.getHeight() / 2; 1784 int endX = startX + 2 * touchSlop; 1785 int endY = startY + 2 * touchSlop; 1786 TestUtils.injectStylusDownEvent(delegatorView, startX, startY); 1787 TestUtils.injectStylusMoveEvents(delegatorView, startX, startY, endX, endY, 5); 1788 1789 try { 1790 expectEvent( 1791 stream, 1792 eventMatcher("onPrepareStylusHandwriting"), 1793 TIMEOUT); 1794 expectEvent( 1795 stream, 1796 eventMatcher("onStartConnectionlessStylusHandwriting"), 1797 TIMEOUT); 1798 verifyStylusHandwritingWindowIsShown(stream, imeSession); 1799 // The transition to show the real edit text shouldn't occur yet. 1800 notExpectEvent( 1801 stream, editorMatcher("onStartInput", delegateMarker), NOT_EXPECT_TIMEOUT); 1802 } finally { 1803 TestUtils.injectStylusUpEvent(delegatorView, endX, endY); 1804 } 1805 imeSession.callFinishConnectionlessStylusHandwriting("abc"); 1806 1807 // Finishing the handwriting session triggers the transition to show the real edit text. 1808 expectEvent( 1809 stream, 1810 eventMatcher("onFinishStylusHandwriting"), 1811 TIMEOUT); 1812 expectEvent(stream, editorMatcher("onStartInput", delegateMarker), TIMEOUT); 1813 // When the real edit text start its input connection, the recognised text from the 1814 // connectionless handwriting session is committed. 1815 EditText delegate = 1816 ((View) delegatorView.getParent()).findViewById(R.id.handwriting_delegate); 1817 TestUtils.waitOnMainUntil(() -> delegate.getText().toString().equals("abc"), 1818 TIMEOUT_IN_SECONDS, "Delegate should receive text"); 1819 } 1820 } 1821 1822 /** 1823 * When the IME supports connectionless handwriting sessions, start a connectionless handwriting 1824 * session for delegation. When the session is finished and a delegate editor view is focused, 1825 * verify that the recognised text is committed to the delegate. 1826 */ 1827 @Test 1828 @ApiTest(apis = { 1829 "android.view.inputmethod.InputMethodManager" 1830 + "#startConnectionlessStylusHandwritingForDelegation", 1831 "android.view.inputmethod.InputMethodManager#acceptStylusHandwritingDelegation", 1832 "android.view.inputmethod.InputMethodService#onStartConnectionlessStylusHandwriting", 1833 "android.view.inputmethod.InputMethodService#finishConnectionlessStylusHandwriting"}) 1834 @FlakyTest(bugId = 328765068) 1835 public void testHandwriting_delegate_connectionless_direct() throws Exception { 1836 final InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); 1837 try (MockImeSession imeSession = MockImeSession.create( 1838 InstrumentationRegistry.getInstrumentation().getContext(), 1839 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 1840 new ImeSettings.Builder().setConnectionlessHandwritingEnabled(true))) { 1841 final ImeEventStream stream = imeSession.openEventStream(); 1842 1843 final String delegateMarker = getTestMarker(); 1844 final View view = 1845 launchTestActivityWithDelegate( 1846 delegateMarker, null /* delegateLatch */, 0 /* delegateDelayMs */); 1847 expectBindInput(stream, Process.myPid(), TIMEOUT); 1848 addVirtualStylusIdForTestSession(); 1849 1850 TestUtils.injectStylusDownEvent(view, 0, 0); 1851 CursorAnchorInfo cursorAnchorInfo = new CursorAnchorInfo.Builder().build(); 1852 TestCallback callback = new TestCallback(); 1853 imm.startConnectionlessStylusHandwritingForDelegation( 1854 view, cursorAnchorInfo, view::post, callback); 1855 1856 expectEvent( 1857 stream, 1858 eventMatcher("onPrepareStylusHandwriting"), 1859 TIMEOUT); 1860 expectEvent( 1861 stream, 1862 eventMatcher("onStartConnectionlessStylusHandwriting"), 1863 TIMEOUT); 1864 verifyStylusHandwritingWindowIsShown(stream, imeSession); 1865 1866 TestUtils.injectStylusUpEvent(view, 0, 0); 1867 imeSession.callFinishConnectionlessStylusHandwriting("abc"); 1868 1869 expectEvent( 1870 stream, 1871 eventMatcher("onFinishStylusHandwriting"), 1872 TIMEOUT); 1873 1874 view.post(() -> view.getHandwritingDelegatorCallback().run()); 1875 1876 expectEvent(stream, editorMatcher("onStartInput", delegateMarker), TIMEOUT); 1877 // When the real edit text start its input connection, the recognised text from the 1878 // connectionless handwriting session is committed. 1879 EditText delegate = 1880 ((View) view.getParent()).findViewById(R.id.handwriting_delegate); 1881 TestUtils.waitOnMainUntil(() -> delegate.getText().toString().equals("abc"), 1882 TIMEOUT_IN_SECONDS, "Delegate should receive text"); 1883 } 1884 } 1885 1886 /** 1887 * Inject stylus events on top of a handwriting initiation delegator view and verify handwriting 1888 * is started on the delegate editor, even though delegate took a little time to 1889 * acceptStylusHandwriting(). 1890 */ 1891 @ApiTest(apis = { 1892 "android.view.inputmethod.InputMethodManager#acceptStylusHandwritingDelegation", 1893 "android.view.inputmethod.InputMethodManager#acceptStylusHandwritingDelegationAsync", 1894 "android.view.inputmethod.InputMethodManager#prepareStylusHandwritingDelegation", 1895 "android.view.View#setHandwritingDelegatorCallback", 1896 "android.view.View#setIsHandwritingDelegate"}) 1897 @Test 1898 public void testHandwriting_delegateDelayed() throws Exception { 1899 try (MockImeSession imeSession = MockImeSession.create( 1900 InstrumentationRegistry.getInstrumentation().getContext(), 1901 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 1902 new ImeSettings.Builder())) { 1903 final ImeEventStream stream = imeSession.openEventStream(); 1904 1905 final String editTextMarker = getTestMarker(); 1906 final CountDownLatch latch = new CountDownLatch(1); 1907 // Use a delegate that executes after 1 second delay. 1908 final View delegatorView = 1909 launchTestActivityWithDelegate(editTextMarker, latch, TIMEOUT_1_S); 1910 expectBindInput(stream, Process.myPid(), TIMEOUT); 1911 addVirtualStylusIdForTestSession(); 1912 1913 final int touchSlop = getTouchSlop(); 1914 final int startX = delegatorView.getWidth() / 2; 1915 final int startY = delegatorView.getHeight() / 2; 1916 final int endX = startX + 2 * touchSlop; 1917 final int endY = startY + 2 * touchSlop; 1918 final int number = 5; 1919 TestUtils.injectStylusDownEvent(delegatorView, startX, startY); 1920 TestUtils.injectStylusMoveEvents(delegatorView, startX, startY, endX, endY, number); 1921 try { 1922 // Wait until delegate makes request. 1923 latch.await(DELEGATION_AFTER_IDLE_TIMEOUT_MS, TimeUnit.MILLISECONDS); 1924 // Keyboard shouldn't show up. 1925 notExpectEvent( 1926 stream, editorMatcher("onStartInputView", editTextMarker), 1927 NOT_EXPECT_TIMEOUT); 1928 // Handwriting should start since delegation was delayed (but still before timeout). 1929 expectEvent( 1930 stream, editorMatcher("onStartStylusHandwriting", editTextMarker), TIMEOUT); 1931 verifyStylusHandwritingWindowIsShown(stream, imeSession); 1932 } finally { 1933 TestUtils.injectStylusUpEvent(delegatorView, endX, endY); 1934 } 1935 } 1936 } 1937 1938 /** 1939 * Inject stylus events on top of a handwriting initiation delegator view and verify handwriting 1940 * is not started on the delegate editor after delegate idle-timeout. 1941 */ 1942 @ApiTest(apis = { 1943 "android.view.inputmethod.InputMethodManager#acceptStylusHandwritingDelegation", 1944 "android.view.inputmethod.InputMethodManager#acceptStylusHandwritingDelegationAsync", 1945 "android.view.inputmethod.InputMethodManager#prepareStylusHandwritingDelegation", 1946 "android.view.View#setHandwritingDelegatorCallback", 1947 "android.view.View#setIsHandwritingDelegate"}) 1948 @Test 1949 public void testHandwriting_delegateAfterTimeout() throws Exception { 1950 try (MockImeSession imeSession = MockImeSession.create( 1951 InstrumentationRegistry.getInstrumentation().getContext(), 1952 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 1953 new ImeSettings.Builder())) { 1954 final ImeEventStream stream = imeSession.openEventStream(); 1955 1956 final String editTextMarker = getTestMarker(); 1957 final CountDownLatch latch = new CountDownLatch(1); 1958 // Use a delegate that executes after idle-timeout. 1959 final View delegatorView = 1960 launchTestActivityWithDelegate( 1961 editTextMarker, latch, DELEGATION_AFTER_IDLE_TIMEOUT_MS); 1962 expectBindInput(stream, Process.myPid(), TIMEOUT); 1963 addVirtualStylusIdForTestSession(); 1964 1965 final int touchSlop = getTouchSlop(); 1966 final int startX = delegatorView.getWidth() / 2; 1967 final int startY = delegatorView.getHeight() / 2; 1968 final int endX = startX + 2 * touchSlop; 1969 final int endY = startY + 2 * touchSlop; 1970 final int number = 5; 1971 TestUtils.injectStylusDownEvent(delegatorView, startX, startY); 1972 TestUtils.injectStylusMoveEvents(delegatorView, startX, startY, endX, endY, number); 1973 try { 1974 // Wait until delegate makes request. 1975 latch.await(DELEGATION_AFTER_IDLE_TIMEOUT_MS, TimeUnit.MILLISECONDS); 1976 // Keyboard shouldn't show up. 1977 notExpectEvent( 1978 stream, editorMatcher("onStartInputView", editTextMarker), 1979 NOT_EXPECT_TIMEOUT); 1980 // Handwriting should *not* start since delegation was idle timed-out. 1981 notExpectEvent( 1982 stream, editorMatcher("onStartStylusHandwriting", editTextMarker), TIMEOUT); 1983 verifyStylusHandwritingWindowIsNotShown(stream, imeSession); 1984 } finally { 1985 TestUtils.injectStylusUpEvent(delegatorView, endX, endY); 1986 } 1987 } 1988 } 1989 1990 /** 1991 * Tap on a view with stylus to launch a new activity with Editor. The editor's 1992 * editor ToolType should match stylus. 1993 */ 1994 @RequiresFlagsEnabled({FLAG_USE_HANDWRITING_LISTENER_FOR_TOOLTYPE, FLAG_DEVICE_ASSOCIATIONS}) 1995 @Test 1996 public void testHandwriting_editorToolTypeOnNewWindow() throws Exception { 1997 Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 1998 try (MockImeSession imeSession = MockImeSession.create( 1999 instrumentation.getContext(), 2000 instrumentation.getUiAutomation(), 2001 new ImeSettings.Builder())) { 2002 final ImeEventStream stream = imeSession.openEventStream(); 2003 2004 final String editTextMarker = getTestMarker(); 2005 final CountDownLatch latch = new CountDownLatch(1); 2006 2007 // Use a clickable view that launches activity and focuses an editor. 2008 final AtomicReference<View> clickableViewRef = new AtomicReference<>(); 2009 final AtomicReference<View> editorViewRef = new AtomicReference<>(); 2010 TestActivity.startSync(activity -> { 2011 final LinearLayout layout = new LinearLayout(activity); 2012 final View clickableView = new View(activity); 2013 clickableViewRef.set(clickableView); 2014 clickableView.setBackgroundColor(Color.GREEN); 2015 clickableView.setOnClickListener(v -> { 2016 final EditText editText = new EditText(activity); 2017 editText.setPrivateImeOptions(editTextMarker); 2018 editText.setHint("editText"); 2019 layout.addView(editText); 2020 editorViewRef.set(editText); 2021 editText.requestFocus(); 2022 latch.countDown(); 2023 }); 2024 2025 LinearLayout.LayoutParams layoutParams = 2026 new LinearLayout.LayoutParams( 2027 LinearLayout.LayoutParams.WRAP_CONTENT, 2028 LinearLayout.LayoutParams.WRAP_CONTENT); 2029 layout.addView(clickableView, layoutParams); 2030 return layout; 2031 }); 2032 addVirtualStylusIdForTestSession(); 2033 View clickableView = clickableViewRef.get(); 2034 Context context = clickableView.getContext(); 2035 2036 expectBindInput(stream, Process.myPid(), TIMEOUT); 2037 // click on view with stylus to launch new activity 2038 try (UinputTouchDevice stylus = 2039 new UinputStylus(instrumentation, clickableView.getDisplay())) { 2040 final int x = clickableView.getWidth() / 2; 2041 final int y = clickableView.getHeight() / 2; 2042 UinputTouchDevice.Pointer stylusPointer = 2043 TestUtils.injectStylusDownEvent(stylus, clickableView, x, y); 2044 TestUtils.injectStylusUpEvent(stylusPointer); 2045 // Wait until editor on next activity has focus. 2046 latch.await(TIMEOUT_1_S, TimeUnit.MILLISECONDS); 2047 2048 // call showSoftInput and make sure onUpdateToolType is stylus. 2049 final InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); 2050 imm.showSoftInput(editorViewRef.get(), 0); 2051 // verify editor on new activity has editorToolType as stylus. 2052 expectEvent( 2053 stream, 2054 onUpdateEditorToolTypeMatcher(MotionEvent.TOOL_TYPE_STYLUS), 2055 TIMEOUT); 2056 } 2057 } 2058 } 2059 2060 /** 2061 * Inject stylus events on top of a handwriting initiation delegate view and verify handwriting 2062 * is started on the delegator editor [in different package] and stylus handwriting is 2063 * started. 2064 * TODO(b/210039666): support instant apps for this test. 2065 */ 2066 @AppModeFull(reason = "Launching external activity from this test is not yet supported.") 2067 @ApiTest(apis = { 2068 "android.view.inputmethod.InputMethodManager#acceptStylusHandwritingDelegation", 2069 "android.view.inputmethod.InputMethodManager#acceptStylusHandwritingDelegationAsync", 2070 "android.view.inputmethod.InputMethodManager#prepareStylusHandwritingDelegation", 2071 "android.view.View#setAllowedHandwritingDelegatePackage", 2072 "android.view.View#setAllowedHandwritingDelegatorPackage", 2073 "android.view.View#setHandwritingDelegatorCallback", 2074 "android.view.View#setIsHandwritingDelegate"}) 2075 @Test 2076 public void testHandwriting_delegateToDifferentPackage() throws Exception { 2077 testHandwriting_delegateToDifferentPackage(true /* setAllowedDelegatorPackage */); 2078 } 2079 2080 /** 2081 * Inject stylus events on top of a handwriting initiation delegate view and verify handwriting 2082 * is not started on the delegator editor [in different package] because allowed package wasn't 2083 * set. 2084 * TODO(b/210039666): support instant apps for this test. 2085 */ 2086 @AppModeFull(reason = "Launching external activity from this test is not yet supported.") 2087 @ApiTest(apis = { 2088 "android.view.inputmethod.InputMethodManager#acceptStylusHandwritingDelegation", 2089 "android.view.inputmethod.InputMethodManager#acceptStylusHandwritingDelegationAsync", 2090 "android.view.inputmethod.InputMethodManager#prepareStylusHandwritingDelegation", 2091 "android.view.View#setAllowedHandwritingDelegatePackage", 2092 "android.view.View#setAllowedHandwritingDelegatorPackage", 2093 "android.view.View#setHandwritingDelegatorCallback", 2094 "android.view.View#setIsHandwritingDelegate"}) 2095 @Test 2096 public void testHandwriting_delegateToDifferentPackage_fail() throws Exception { 2097 testHandwriting_delegateToDifferentPackage(false /* setAllowedDelegatorPackage */); 2098 } 2099 2100 private void testHandwriting_delegateToDifferentPackage(boolean setAllowedDelegatorPackage) 2101 throws Exception { 2102 try (MockImeSession imeSession = MockImeSession.create( 2103 InstrumentationRegistry.getInstrumentation().getContext(), 2104 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 2105 new ImeSettings.Builder())) { 2106 final ImeEventStream stream = imeSession.openEventStream(); 2107 2108 final String editTextMarker = getTestMarker(); 2109 final View delegateView = 2110 launchTestActivityInExternalPackage(editTextMarker, setAllowedDelegatorPackage); 2111 expectBindInput(stream, Process.myPid(), TIMEOUT); 2112 addVirtualStylusIdForTestSession(); 2113 2114 final int touchSlop = getTouchSlop(); 2115 final int startX = delegateView.getWidth() / 2; 2116 final int startY = delegateView.getHeight() / 2; 2117 final int endX = startX + 2 * touchSlop; 2118 final int endY = startY + 2 * touchSlop; 2119 final int number = 5; 2120 2121 TestUtils.injectStylusDownEvent(delegateView, startX, startY); 2122 TestUtils.injectStylusMoveEvents(delegateView, startX, startY, endX, endY, number); 2123 2124 try { 2125 // Keyboard shouldn't show up. 2126 notExpectEvent( 2127 stream, editorMatcher("onStartInputView", editTextMarker), 2128 NOT_EXPECT_TIMEOUT); 2129 2130 if (setAllowedDelegatorPackage) { 2131 if (mFlagsValueProvider.getBoolean(FLAG_INITIATION_WITHOUT_INPUT_CONNECTION)) { 2132 // There will be no active InputConnection when handwriting starts 2133 expectEvent( 2134 stream, 2135 eventMatcher("onStartStylusHandwriting"), 2136 TIMEOUT); 2137 } else { 2138 expectEvent( 2139 stream, editorMatcher("onStartStylusHandwriting", editTextMarker), 2140 TIMEOUT); 2141 } 2142 verifyStylusHandwritingWindowIsShown(stream, imeSession); 2143 } else { 2144 if (mFlagsValueProvider.getBoolean(FLAG_INITIATION_WITHOUT_INPUT_CONNECTION)) { 2145 // There will be no active InputConnection if handwriting starts 2146 notExpectEvent( 2147 stream, 2148 eventMatcher("onStartStylusHandwriting"), 2149 NOT_EXPECT_TIMEOUT); 2150 } else { 2151 notExpectEvent( 2152 stream, editorMatcher("onStartStylusHandwriting", editTextMarker), 2153 NOT_EXPECT_TIMEOUT); 2154 } 2155 } 2156 } finally { 2157 TestUtils.injectStylusUpEvent(delegateView, endX, endY); 2158 } 2159 } 2160 } 2161 2162 /** 2163 * Inject stylus events on top of a handwriting initiation delegator view in the default 2164 * launcher activity, and verify stylus handwriting is started on the delegate editor (in a 2165 * different package]. 2166 * TODO(b/210039666): Support instant apps for this test. 2167 */ 2168 @Test 2169 @ApiTest(apis = { 2170 "android.view.inputmethod.InputMethodManager#acceptStylusHandwritingDelegation", 2171 "android.view.inputmethod.InputMethodManager#acceptStylusHandwritingDelegationAsync", 2172 "android.view.inputmethod.InputMethodManager#prepareStylusHandwritingDelegation", 2173 "android.view.View#setAllowedHandwritingDelegatePackage", 2174 "android.view.View#setAllowedHandwritingDelegatorPackage", 2175 "android.view.View#setHandwritingDelegateFlags", 2176 "android.view.View#setHandwritingDelegatorCallback", 2177 "android.view.View#setIsHandwritingDelegate"}) 2178 @RequiresFlagsEnabled(FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR) 2179 @AppModeFull(reason = "Launching external activity from this test is not yet supported.") 2180 public void testHandwriting_delegateFromHomePackage() throws Exception { 2181 testHandwriting_delegateFromHomePackage(/* setHomeDelegatorAllowed= */ true); 2182 } 2183 2184 /** 2185 * Inject stylus events on top of a handwriting initiation delegator view in the default 2186 * launcher activity, and verify stylus handwriting is not started on the delegate editor (in a 2187 * different package] because {@link View#setHomeScreenHandwritingDelegatorAllowed} wasn't set. 2188 * TODO(b/210039666): Support instant apps for this test. 2189 */ 2190 @Test 2191 @ApiTest(apis = { 2192 "android.view.inputmethod.InputMethodManager#acceptStylusHandwritingDelegation", 2193 "android.view.inputmethod.InputMethodManager#acceptStylusHandwritingDelegationAsync", 2194 "android.view.inputmethod.InputMethodManager#prepareStylusHandwritingDelegation", 2195 "android.view.View#setAllowedHandwritingDelegatePackage", 2196 "android.view.View#setAllowedHandwritingDelegatorPackage", 2197 "android.view.View#setHandwritingDelegateFlags", 2198 "android.view.View#setHandwritingDelegatorCallback", 2199 "android.view.View#setIsHandwritingDelegate"}) 2200 @RequiresFlagsEnabled(FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR) 2201 @AppModeFull(reason = "Launching external activity from this test is not yet supported.") 2202 public void testHandwriting_delegateFromHomePackage_fail() throws Exception { 2203 testHandwriting_delegateFromHomePackage(/* setHomeDelegatorAllowed= */ false); 2204 } 2205 2206 public void testHandwriting_delegateFromHomePackage(boolean setHomeDelegatorAllowed) 2207 throws Exception { 2208 mDefaultLauncherToRestore = getDefaultLauncher(); 2209 setDefaultLauncher(TEST_LAUNCHER_COMPONENT); 2210 2211 try (MockImeSession imeSession = MockImeSession.create( 2212 InstrumentationRegistry.getInstrumentation().getContext(), 2213 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 2214 new ImeSettings.Builder())) { 2215 ImeEventStream stream = imeSession.openEventStream(); 2216 2217 String editTextMarker = getTestMarker(); 2218 2219 // Start launcher activity 2220 Intent intent = new Intent(Intent.ACTION_MAIN); 2221 intent.addCategory(Intent.CATEGORY_HOME); 2222 intent.addCategory(Intent.CATEGORY_DEFAULT); 2223 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 2224 // LauncherActivity passes these three extras to the ctstestapp MainActivity 2225 intent.putExtra(MockTestActivityUtil.EXTRA_KEY_PRIVATE_IME_OPTIONS, editTextMarker); 2226 intent.putExtra(MockTestActivityUtil.EXTRA_HANDWRITING_DELEGATE, true); 2227 intent.putExtra( 2228 MockTestActivityUtil.EXTRA_HOME_HANDWRITING_DELEGATOR_ALLOWED, 2229 setHomeDelegatorAllowed); 2230 InstrumentationRegistry.getInstrumentation().getContext().startActivity(intent); 2231 2232 expectBindInput(stream, Process.myPid(), TIMEOUT); 2233 addVirtualStylusIdForTestSession(); 2234 2235 // Launcher activity displays a full screen handwriting delegator view. Stylus events 2236 // are injected in the center of the screen to trigger the delegator callback, which 2237 // launches the ctstestapp MainActivity with the delegate editor with editTextMarker. 2238 DisplayMetrics metrics = mContext.getResources().getDisplayMetrics(); 2239 int touchSlop = getTouchSlop(); 2240 int startX = metrics.widthPixels / 2; 2241 int startY = metrics.heightPixels / 2; 2242 int endX = startX + 2 * touchSlop; 2243 int endY = startY + 2 * touchSlop; 2244 View mockView = mock(View.class); 2245 TestUtils.injectStylusDownEvent(mockView, startX, startY); 2246 TestUtils.injectStylusMoveEvents(mockView, startX, startY, endX, endY, 5); 2247 2248 try { 2249 // Keyboard shouldn't show up. 2250 notExpectEvent( 2251 stream, editorMatcher("onStartInputView", editTextMarker), 2252 NOT_EXPECT_TIMEOUT); 2253 if (setHomeDelegatorAllowed) { 2254 if (mFlagsValueProvider.getBoolean(FLAG_INITIATION_WITHOUT_INPUT_CONNECTION)) { 2255 // There will be no active InputConnection when handwriting starts. 2256 expectEvent( 2257 stream, 2258 eventMatcher("onStartStylusHandwriting"), 2259 TIMEOUT); 2260 } else { 2261 expectEvent( 2262 stream, editorMatcher("onStartStylusHandwriting", editTextMarker), 2263 TIMEOUT); 2264 } 2265 verifyStylusHandwritingWindowIsShown(stream, imeSession); 2266 } else { 2267 if (mFlagsValueProvider.getBoolean(FLAG_INITIATION_WITHOUT_INPUT_CONNECTION)) { 2268 // There will be no active InputConnection if handwriting starts. 2269 notExpectEvent( 2270 stream, 2271 eventMatcher("onStartStylusHandwriting"), 2272 NOT_EXPECT_TIMEOUT); 2273 } else { 2274 notExpectEvent( 2275 stream, editorMatcher("onStartStylusHandwriting", editTextMarker), 2276 NOT_EXPECT_TIMEOUT); 2277 } 2278 } 2279 } finally { 2280 TestUtils.injectStylusUpEvent(mockView, endX, endY); 2281 } 2282 } 2283 } 2284 2285 @Test 2286 @RequiresFlagsEnabled(FLAG_CONNECTIONLESS_HANDWRITING) 2287 @ApiTest(apis = { 2288 "android.view.inputmethod.InputMethodManager#startConnectionlessStylusHandwriting", 2289 "android.view.inputmethod.InputMethodManager#onStartConnectionlessStylusHandwriting", 2290 "android.view.inputmethod.InputMethodManager#finishConnectionlessStylusHandwriting"}) 2291 public void testHandwriting_connectionless_standalone() throws Exception { 2292 final InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); 2293 try (MockImeSession imeSession = MockImeSession.create( 2294 InstrumentationRegistry.getInstrumentation().getContext(), 2295 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 2296 new ImeSettings.Builder().setConnectionlessHandwritingEnabled(true))) { 2297 final ImeEventStream stream = imeSession.openEventStream(); 2298 2299 final View view = 2300 launchTestActivityWithDelegate( 2301 getTestMarker(), null /* delegateLatch */, 0 /* delegateDelayMs */); 2302 expectBindInput(stream, Process.myPid(), TIMEOUT); 2303 addVirtualStylusIdForTestSession(); 2304 2305 TestUtils.injectStylusDownEvent(view, 0, 0); 2306 try { 2307 CursorAnchorInfo cursorAnchorInfo = new CursorAnchorInfo.Builder().build(); 2308 TestCallback callback = new TestCallback(); 2309 imm.startConnectionlessStylusHandwriting(view, cursorAnchorInfo, view::post, 2310 callback); 2311 2312 expectEvent( 2313 stream, 2314 eventMatcher("onPrepareStylusHandwriting"), 2315 TIMEOUT); 2316 expectEvent( 2317 stream, 2318 eventMatcher("onStartConnectionlessStylusHandwriting"), 2319 TIMEOUT); 2320 verifyStylusHandwritingWindowIsShown(stream, imeSession); 2321 2322 imeSession.callFinishConnectionlessStylusHandwriting("abc"); 2323 2324 expectEvent( 2325 stream, 2326 eventMatcher("onFinishStylusHandwriting"), 2327 TIMEOUT); 2328 assertThat(callback.mResultText).isEqualTo("abc"); 2329 assertThat(callback.mErrorCode).isEqualTo(-1); 2330 } finally { 2331 TestUtils.injectStylusUpEvent(view, 0, 0); 2332 } 2333 } 2334 } 2335 2336 @Test 2337 @RequiresFlagsEnabled(FLAG_CONNECTIONLESS_HANDWRITING) 2338 @ApiTest(apis = { 2339 "android.view.inputmethod.InputMethodManager#startConnectionlessStylusHandwriting", 2340 "android.view.inputmethod.InputMethodManager#onStartConnectionlessStylusHandwriting", 2341 "android.view.inputmethod.InputMethodManager#finishConnectionlessStylusHandwriting"}) 2342 public void testHandwriting_connectionless_standalone_error() throws Exception { 2343 final InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); 2344 try (MockImeSession imeSession = MockImeSession.create( 2345 InstrumentationRegistry.getInstrumentation().getContext(), 2346 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 2347 new ImeSettings.Builder().setConnectionlessHandwritingEnabled(true))) { 2348 final ImeEventStream stream = imeSession.openEventStream(); 2349 2350 final View view = 2351 launchTestActivityWithDelegate( 2352 getTestMarker(), null /* delegateLatch */, 0 /* delegateDelayMs */); 2353 expectBindInput(stream, Process.myPid(), TIMEOUT); 2354 addVirtualStylusIdForTestSession(); 2355 2356 TestUtils.injectStylusDownEvent(view, 0, 0); 2357 try { 2358 CursorAnchorInfo cursorAnchorInfo = new CursorAnchorInfo.Builder().build(); 2359 TestCallback callback = new TestCallback(); 2360 imm.startConnectionlessStylusHandwriting(view, cursorAnchorInfo, view::post, 2361 callback); 2362 2363 expectEvent( 2364 stream, 2365 eventMatcher("onPrepareStylusHandwriting"), 2366 TIMEOUT); 2367 expectEvent( 2368 stream, 2369 eventMatcher("onStartConnectionlessStylusHandwriting"), 2370 TIMEOUT); 2371 verifyStylusHandwritingWindowIsShown(stream, imeSession); 2372 2373 // Finish the session with no text recognized. 2374 imeSession.callFinishConnectionlessStylusHandwriting(""); 2375 2376 expectEvent( 2377 stream, 2378 eventMatcher("onFinishStylusHandwriting"), 2379 TIMEOUT); 2380 assertThat(callback.mResultText).isNull(); 2381 assertThat(callback.mErrorCode) 2382 .isEqualTo(CONNECTIONLESS_HANDWRITING_ERROR_NO_TEXT_RECOGNIZED); 2383 } finally { 2384 TestUtils.injectStylusUpEvent(view, 0, 0); 2385 } 2386 } 2387 } 2388 2389 @Test 2390 @RequiresFlagsEnabled(FLAG_CONNECTIONLESS_HANDWRITING) 2391 @ApiTest(apis = { 2392 "android.view.inputmethod.InputMethodManager#startConnectionlessStylusHandwriting", 2393 "android.view.inputmethod.InputMethodService#onStartConnectionlessStylusHandwriting", 2394 "android.view.inputmethod.InputMethodManager#finishConnectionlessStylusHandwriting"}) 2395 public void testHandwriting_connectionless_standalone_unsupported() throws Exception { 2396 final InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); 2397 try (MockImeSession imeSession = MockImeSession.create( 2398 InstrumentationRegistry.getInstrumentation().getContext(), 2399 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 2400 new ImeSettings.Builder().setConnectionlessHandwritingEnabled(false))) { 2401 final ImeEventStream stream = imeSession.openEventStream(); 2402 2403 final View view = 2404 launchTestActivityWithDelegate( 2405 getTestMarker(), null /* delegateLatch */, 0 /* delegateDelayMs */); 2406 expectBindInput(stream, Process.myPid(), TIMEOUT); 2407 addVirtualStylusIdForTestSession(); 2408 2409 TestUtils.injectStylusDownEvent(view, 0, 0); 2410 try { 2411 CursorAnchorInfo cursorAnchorInfo = new CursorAnchorInfo.Builder().build(); 2412 TestCallback callback = new TestCallback(); 2413 imm.startConnectionlessStylusHandwriting(view, cursorAnchorInfo, view::post, 2414 callback); 2415 2416 // onPrepareStylusHandwriting and onStartConnectionlessStylusHandwriting are called, 2417 // but onStartConnectionlessStylusHandwriting returns false so handwriting 2418 // does not start. 2419 expectEvent( 2420 stream, 2421 eventMatcher("onPrepareStylusHandwriting"), 2422 TIMEOUT); 2423 expectEvent( 2424 stream, 2425 eventMatcher("onStartConnectionlessStylusHandwriting"), 2426 TIMEOUT); 2427 verifyStylusHandwritingWindowIsNotShown(stream, imeSession); 2428 assertThat(callback.mResultText).isNull(); 2429 assertThat(callback.mErrorCode) 2430 .isEqualTo(CONNECTIONLESS_HANDWRITING_ERROR_UNSUPPORTED); 2431 } finally { 2432 TestUtils.injectStylusUpEvent(view, 0, 0); 2433 } 2434 } 2435 } 2436 2437 /** 2438 * Verify that system times-out Handwriting session after given timeout. 2439 */ 2440 @Test 2441 public void testHandwritingSessionIdleTimeout() throws Exception { 2442 try (MockImeSession imeSession = MockImeSession.create( 2443 InstrumentationRegistry.getInstrumentation().getContext(), 2444 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 2445 new ImeSettings.Builder())) { 2446 final ImeEventStream stream = imeSession.openEventStream(); 2447 2448 final String marker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 2449 final EditText editText = launchTestActivity(marker); 2450 2451 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 2452 notExpectEvent( 2453 stream, 2454 editorMatcher("onStartInputView", marker), 2455 NOT_EXPECT_TIMEOUT); 2456 2457 addVirtualStylusIdForTestSession(); 2458 // update handwriting session timeout 2459 assertTrue(expectCommand( 2460 stream, 2461 imeSession.callSetStylusHandwritingTimeout(100 /* timeoutMs */), 2462 TIMEOUT).getReturnBooleanValue()); 2463 2464 injectStylusEventToEditorAndVerify(editText, stream, imeSession, 2465 marker, true /* verifyHandwritingStart */, 2466 false /* verifyHandwritingWindowShown */, 2467 false /* verifyHandwritingWindowNotShown */); 2468 2469 // Handwriting should finish soon. 2470 expectEvent( 2471 stream, 2472 editorMatcher("onFinishStylusHandwriting", marker), 2473 TIMEOUT_1_S); 2474 2475 // test setting extremely large timeout and verify we limit it to 2476 // STYLUS_HANDWRITING_IDLE_TIMEOUT_MS 2477 assertTrue(expectCommand( 2478 stream, imeSession.callSetStylusHandwritingTimeout( 2479 InputMethodService.getStylusHandwritingIdleTimeoutMax().toMillis() 2480 * 10), 2481 TIMEOUT).getReturnBooleanValue()); 2482 assertEquals("Stylus handwriting timeout must be equal to max value.", 2483 InputMethodService.getStylusHandwritingIdleTimeoutMax().toMillis(), 2484 expectCommand( 2485 stream, imeSession.callGetStylusHandwritingTimeout(), TIMEOUT) 2486 .getReturnLongValue()); 2487 } 2488 } 2489 2490 @Test 2491 public void testHandwritingFinishesOnUnbind() throws Exception { 2492 try (MockImeSession imeSession = MockImeSession.create( 2493 InstrumentationRegistry.getInstrumentation().getContext(), 2494 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 2495 new ImeSettings.Builder())) { 2496 final ImeEventStream stream = imeSession.openEventStream(); 2497 2498 final String marker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 2499 final EditText editText = launchTestActivity(marker); 2500 2501 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 2502 notExpectEvent( 2503 stream, 2504 editorMatcher("onStartInputView", marker), 2505 NOT_EXPECT_TIMEOUT); 2506 2507 addVirtualStylusIdForTestSession(); 2508 2509 final int touchSlop = getTouchSlop(); 2510 final int startX = editText.getWidth() / 2; 2511 final int startY = editText.getHeight() / 2; 2512 final int endX = startX + 2 * touchSlop; 2513 final int endY = startY; 2514 final int number = 5; 2515 TestUtils.injectStylusDownEvent(editText, startX, startY); 2516 TestUtils.injectStylusMoveEvents(editText, startX, startY, 2517 endX, endY, number); 2518 2519 try { 2520 expectEvent( 2521 stream, 2522 editorMatcher("onStartStylusHandwriting", marker), 2523 TIMEOUT); 2524 // Unbind IME and verify finish is called 2525 ((Activity) editText.getContext()).finish(); 2526 2527 // Handwriting should finish soon. 2528 expectEvent( 2529 stream, 2530 editorMatcher("onFinishStylusHandwriting", marker), 2531 TIMEOUT_1_S); 2532 verifyStylusHandwritingWindowIsNotShown(stream, imeSession); 2533 } finally { 2534 TestUtils.injectStylusUpEvent(editText, endX, endY); 2535 } 2536 } 2537 } 2538 2539 /** 2540 * Verify that system remove handwriting window immediately when timeout is small 2541 */ 2542 @Test 2543 public void testHandwritingWindowRemoval_immediate() throws Exception { 2544 final InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); 2545 try (MockImeSession imeSession = MockImeSession.create( 2546 InstrumentationRegistry.getInstrumentation().getContext(), 2547 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 2548 new ImeSettings.Builder())) { 2549 final ImeEventStream stream = imeSession.openEventStream(); 2550 2551 final String marker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 2552 final EditText editText = launchTestActivity(marker); 2553 2554 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 2555 notExpectEvent( 2556 stream, 2557 editorMatcher("onStartInputView", marker), 2558 NOT_EXPECT_TIMEOUT); 2559 2560 addVirtualStylusIdForTestSession(); 2561 // update handwriting window timeout to a small value so that it is removed immediately. 2562 SystemUtil.runWithShellPermissionIdentity(() -> 2563 imm.setStylusWindowIdleTimeoutForTest(100)); 2564 2565 final int touchSlop = getTouchSlop(); 2566 final int startX = editText.getWidth() / 2; 2567 final int startY = editText.getHeight() / 2; 2568 final int endX = startX + 2 * touchSlop; 2569 final int endY = startY; 2570 final int number = 5; 2571 TestUtils.injectStylusDownEvent(editText, startX, startY); 2572 TestUtils.injectStylusMoveEvents(editText, startX, startY, 2573 endX, endY, number); 2574 try { 2575 // Handwriting should already be initiated before ACTION_UP. 2576 // keyboard shouldn't show up. 2577 notExpectEvent( 2578 stream, 2579 editorMatcher("onStartInputView", marker), 2580 NOT_EXPECT_TIMEOUT); 2581 // Handwriting should start 2582 expectEvent( 2583 stream, 2584 editorMatcher("onStartStylusHandwriting", marker), 2585 TIMEOUT); 2586 } finally { 2587 TestUtils.injectStylusUpEvent(editText, endX, endY); 2588 } 2589 2590 // Handwriting should finish soon. 2591 expectEvent( 2592 stream, 2593 editorMatcher("onFinishStylusHandwriting", marker), 2594 TIMEOUT_1_S); 2595 verifyStylusHandwritingWindowIsNotShown(stream, imeSession); 2596 // Verify handwriting window is removed. 2597 assertFalse(expectCommand( 2598 stream, imeSession.callHasStylusHandwritingWindow(), TIMEOUT_1_S) 2599 .getReturnBooleanValue()); 2600 } 2601 } 2602 2603 2604 /** 2605 * Verify that system remove handwriting window after timeout 2606 */ 2607 @Test 2608 public void testHandwritingWindowRemoval_afterDelay() throws Exception { 2609 final InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); 2610 try (MockImeSession imeSession = MockImeSession.create( 2611 InstrumentationRegistry.getInstrumentation().getContext(), 2612 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 2613 new ImeSettings.Builder())) { 2614 // skip this test if device doesn't have stylus. 2615 // stylus is required, otherwise stylus virtual deviceId is removed on finishInput and 2616 // we cannot test InkWindow living beyond finishHandwriting. 2617 assumeTrue("Skipping test on devices that don't have stylus connected.", 2618 hasSupportedStylus()); 2619 final ImeEventStream stream = imeSession.openEventStream(); 2620 2621 final String marker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 2622 final EditText editText = launchTestActivity(marker); 2623 2624 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 2625 notExpectEvent( 2626 stream, 2627 editorMatcher("onStartInputView", marker), 2628 NOT_EXPECT_TIMEOUT); 2629 2630 final int touchSlop = getTouchSlop(); 2631 final int startX = editText.getWidth() / 2; 2632 final int startY = editText.getHeight() / 2; 2633 final int endX = startX + 2 * touchSlop; 2634 final int endY = startY; 2635 final int number = 5; 2636 2637 // Set a larger timeout and verify handwriting window exists after unbind. 2638 SystemUtil.runWithShellPermissionIdentity(() -> 2639 imm.setStylusWindowIdleTimeoutForTest(TIMEOUT)); 2640 2641 TestUtils.injectStylusDownEvent(editText, startX, startY); 2642 TestUtils.injectStylusMoveEvents(editText, startX, startY, 2643 endX, endY, number); 2644 try { 2645 // Handwriting should already be initiated before ACTION_UP. 2646 // Handwriting should start 2647 expectEvent( 2648 stream, 2649 editorMatcher("onStartStylusHandwriting", marker), 2650 TIMEOUT); 2651 } finally { 2652 TestUtils.injectStylusUpEvent(editText, endX, endY); 2653 } 2654 2655 // Handwriting should finish soon. 2656 notExpectEvent( 2657 stream, 2658 editorMatcher("onFinishStylusHandwriting", marker), 2659 TIMEOUT_1_S); 2660 verifyStylusHandwritingWindowIsShown(stream, imeSession); 2661 // Verify handwriting window exists. 2662 assertTrue(expectCommand( 2663 stream, imeSession.callHasStylusHandwritingWindow(), TIMEOUT_1_S) 2664 .getReturnBooleanValue()); 2665 2666 // Finish activity and IME window should be invisible. 2667 ((Activity) editText.getContext()).finish(); 2668 verifyStylusHandwritingWindowIsNotShown(stream, imeSession); 2669 // Verify handwriting window isn't removed immediately. 2670 assertTrue(expectCommand( 2671 stream, imeSession.callHasStylusHandwritingWindow(), TIMEOUT_1_S) 2672 .getReturnBooleanValue()); 2673 // Verify handwriting window is eventually removed (within timeout). 2674 CommonTestUtils.waitUntil("Stylus handwriting window should be removed", 2675 TIMEOUT_IN_SECONDS, 2676 () -> !expectCommand( 2677 stream, imeSession.callHasStylusHandwritingWindow(), TIMEOUT) 2678 .getReturnBooleanValue()); 2679 } 2680 } 2681 2682 /** 2683 * Verify that when system has no stylus, there is no handwriting window. 2684 */ 2685 @Test 2686 @ApiTest(apis = {"android.view.inputmethod.InputMethodManager#startStylusHandwriting", 2687 "android.inputmethodservice.InputMethodService#onStartStylusHandwriting", 2688 "android.inputmethodservice.InputMethodService#onFinishStylusHandwriting"}) 2689 public void testNoStylusNoHandwritingWindow() throws Exception { 2690 // skip this test if device already has stylus. 2691 assumeFalse("Skipping test on devices that have stylus connected.", 2692 hasSupportedStylus()); 2693 2694 try (MockImeSession imeSession = MockImeSession.create( 2695 InstrumentationRegistry.getInstrumentation().getContext(), 2696 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 2697 new ImeSettings.Builder())) { 2698 final ImeEventStream stream = imeSession.openEventStream(); 2699 2700 final String marker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 2701 final EditText editText = launchTestActivity(marker); 2702 2703 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 2704 notExpectEvent( 2705 stream, 2706 editorMatcher("onStartInputView", marker), 2707 NOT_EXPECT_TIMEOUT); 2708 2709 // Verify there is no handwriting window before stylus is added. 2710 assertFalse(expectCommand( 2711 stream, imeSession.callHasStylusHandwritingWindow(), TIMEOUT_1_S) 2712 .getReturnBooleanValue()); 2713 2714 addVirtualStylusIdForTestSession(); 2715 2716 injectStylusEventToEditorAndVerify(editText, stream, imeSession, 2717 marker, true /* verifyHandwritingStart */, 2718 true /* verifyHandwritingWindowShown */, 2719 false /* verifyHandwritingWindowNotShown */); 2720 2721 // Finish handwriting to remove test stylus id. 2722 imeSession.callFinishStylusHandwriting(); 2723 expectEvent( 2724 stream, 2725 editorMatcher("onFinishStylusHandwriting", marker), 2726 TIMEOUT_1_S); 2727 2728 // Verify no handwriting window after stylus is removed from device. 2729 assertFalse(expectCommand( 2730 stream, imeSession.callHasStylusHandwritingWindow(), TIMEOUT_1_S) 2731 .getReturnBooleanValue()); 2732 2733 } 2734 } 2735 2736 /** 2737 * Verifies that in split-screen multi-window mode, unfocused activity can start handwriting 2738 */ 2739 @Test 2740 @ApiTest(apis = {"android.view.inputmethod.InputMethodManager#startStylusHandwriting", 2741 "android.inputmethodservice.InputMethodService#onStartStylusHandwriting", 2742 "android.inputmethodservice.InputMethodService#onFinishStylusHandwriting"}) 2743 public void testMultiWindow_unfocusedWindowCanStartHandwriting() throws Exception { 2744 assumeTrue(TestUtils.supportsSplitScreenMultiWindow()); 2745 2746 try (MockImeSession imeSession = MockImeSession.create( 2747 InstrumentationRegistry.getInstrumentation().getContext(), 2748 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 2749 new ImeSettings.Builder())) { 2750 final ImeEventStream stream = imeSession.openEventStream(); 2751 final String primaryMarker = getTestMarker(FIRST_EDIT_TEXT_TAG); 2752 final String secondaryMarker = getTestMarker(SECOND_EDIT_TEXT_TAG); 2753 2754 // Launch an editor activity to be on the split primary task. 2755 final TestActivity splitPrimaryActivity = TestActivity.startSync(activity -> { 2756 final LinearLayout layout = new LinearLayout(activity); 2757 layout.setOrientation(LinearLayout.VERTICAL); 2758 final EditText editText = new EditText(activity); 2759 layout.addView(editText); 2760 editText.setHint("focused editText"); 2761 editText.setPrivateImeOptions(primaryMarker); 2762 editText.requestFocus(); 2763 return layout; 2764 }); 2765 expectEvent(stream, editorMatcher("onStartInput", primaryMarker), TIMEOUT); 2766 notExpectEvent(stream, editorMatcher("onStartInputView", primaryMarker), 2767 NOT_EXPECT_TIMEOUT); 2768 2769 TestUtils.waitOnMainUntil(() -> splitPrimaryActivity.hasWindowFocus(), TIMEOUT); 2770 2771 // Launch another activity to be on the split secondary task, expect stylus gesture on 2772 // it can steal focus from primary and start handwriting. 2773 final AtomicReference<EditText> editTextRef = new AtomicReference<>(); 2774 final TestActivity splitSecondaryActivity = new TestActivity.Starter() 2775 .asMultipleTask() 2776 .withAdditionalFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT) 2777 .startSync(splitPrimaryActivity, activity -> { 2778 final LinearLayout layout = new LinearLayout(activity); 2779 layout.setOrientation(LinearLayout.VERTICAL); 2780 final EditText editText = new EditText(activity); 2781 editTextRef.set(editText); 2782 layout.addView(editText); 2783 editText.setHint("unfocused editText"); 2784 editText.setPrivateImeOptions(secondaryMarker); 2785 return layout; 2786 }, TestActivity2.class); 2787 notExpectEvent(stream, eventMatcher("onStartInputView"), 2788 NOT_EXPECT_TIMEOUT); 2789 TestUtils.waitOnMainUntil(() -> splitSecondaryActivity.hasWindowFocus(), TIMEOUT); 2790 TestUtils.waitOnMainUntil(() -> !splitPrimaryActivity.hasWindowFocus(), TIMEOUT_1_S); 2791 2792 addVirtualStylusIdForTestSession(); 2793 2794 final EditText editText = editTextRef.get(); 2795 2796 injectStylusEventToEditorAndVerify(editText, stream, imeSession, 2797 secondaryMarker, true /* verifyHandwritingStart */, 2798 true /* verifyHandwritingWindowShown */, 2799 false /* verifyHandwritingWindowNotShown */); 2800 2801 // Finish handwriting to remove test stylus id. 2802 imeSession.callFinishStylusHandwriting(); 2803 expectEvent( 2804 stream, 2805 editorMatcher("onFinishStylusHandwriting", secondaryMarker), 2806 TIMEOUT_1_S); 2807 } 2808 } 2809 2810 /** 2811 * Verifies that in split-screen multi-window mode, an unfocused window can't steal ongoing 2812 * handwriting session. 2813 */ 2814 @Test 2815 @ApiTest(apis = {"android.view.inputmethod.InputMethodManager#startStylusHandwriting", 2816 "android.inputmethodservice.InputMethodService#onStartStylusHandwriting", 2817 "android.inputmethodservice.InputMethodService#onFinishStylusHandwriting"}) 2818 public void testMultiWindow_unfocusedWindowCannotStealOngoingHandwriting() throws Exception { 2819 assumeTrue(TestUtils.supportsSplitScreenMultiWindow()); 2820 2821 try (MockImeSession imeSession = MockImeSession.create( 2822 InstrumentationRegistry.getInstrumentation().getContext(), 2823 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 2824 new ImeSettings.Builder())) { 2825 final ImeEventStream stream = imeSession.openEventStream(); 2826 final String primaryMarker = getTestMarker(FIRST_EDIT_TEXT_TAG); 2827 final String secondaryMarker = getTestMarker(SECOND_EDIT_TEXT_TAG); 2828 2829 // Launch an editor activity to be on the split primary task. 2830 final AtomicReference<EditText> editTextPrimaryRef = new AtomicReference<>(); 2831 final TestActivity splitPrimaryActivity = TestActivity.startSync(activity -> { 2832 final LinearLayout layout = new LinearLayout(activity); 2833 layout.setOrientation(LinearLayout.VERTICAL); 2834 final EditText editText = new EditText(activity); 2835 layout.addView(editText); 2836 editTextPrimaryRef.set(editText); 2837 editText.setHint("focused editText"); 2838 editText.setPrivateImeOptions(primaryMarker); 2839 return layout; 2840 }); 2841 notExpectEvent(stream, 2842 editorMatcher("onStartInput", primaryMarker), NOT_EXPECT_TIMEOUT); 2843 notExpectEvent(stream, 2844 editorMatcher("onStartInputView", primaryMarker), NOT_EXPECT_TIMEOUT); 2845 2846 TestUtils.waitOnMainUntil(() -> splitPrimaryActivity.hasWindowFocus(), TIMEOUT); 2847 2848 // Launch another activity to be on the split secondary task, expect stylus gesture on 2849 // it can steal focus from primary and start handwriting. 2850 final AtomicReference<EditText> editTextSecondaryRef = new AtomicReference<>(); 2851 new TestActivity.Starter() 2852 .asMultipleTask() 2853 .withAdditionalFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT) 2854 .startSync(splitPrimaryActivity, activity -> { 2855 final LinearLayout layout = new LinearLayout(activity); 2856 layout.setOrientation(LinearLayout.VERTICAL); 2857 final EditText editText = new EditText(activity); 2858 editTextSecondaryRef.set(editText); 2859 layout.addView(editText); 2860 editText.setHint("unfocused editText"); 2861 editText.setPrivateImeOptions(secondaryMarker); 2862 return layout; 2863 }, TestActivity2.class); 2864 notExpectEvent(stream, eventMatcher("onStartInputView"), NOT_EXPECT_TIMEOUT); 2865 2866 addVirtualStylusIdForTestSession(); 2867 2868 // Inject events on primary to start handwriting. 2869 final EditText editTextPrimary = editTextPrimaryRef.get(); 2870 2871 injectStylusEventToEditorAndVerify(editTextPrimary, stream, imeSession, 2872 primaryMarker, true /* verifyHandwritingStart */, 2873 false /* verifyHandwritingWindowShown */, 2874 false /* verifyHandwritingWindowNotShown */); 2875 2876 TestUtils.waitOnMainUntil(() -> splitPrimaryActivity.hasWindowFocus(), TIMEOUT_1_S); 2877 2878 // Inject events on secondary shouldn't start handwriting on secondary 2879 // (since primary is already ongoing). 2880 final EditText editTextSecondary = editTextSecondaryRef.get(); 2881 2882 injectStylusEventToEditorAndVerify(editTextSecondary, stream, imeSession, 2883 secondaryMarker, false /* verifyHandwritingStart */, 2884 false /* verifyHandwritingWindowShown */, 2885 false /* verifyHandwritingWindowNotShown */); 2886 2887 TestUtils.waitOnMainUntil(() -> splitPrimaryActivity.hasWindowFocus(), TIMEOUT_1_S); 2888 2889 // Finish handwriting to remove test stylus id. 2890 imeSession.callFinishStylusHandwriting(); 2891 expectEvent( 2892 stream, 2893 editorMatcher("onFinishStylusHandwriting", primaryMarker), 2894 TIMEOUT_1_S); 2895 } 2896 } 2897 2898 /** 2899 * Verify that once stylus hasn't been used for more than idle-timeout, there is no handwriting 2900 * window. 2901 */ 2902 @Test 2903 @ApiTest(apis = {"android.view.inputmethod.InputMethodManager#startStylusHandwriting", 2904 "android.inputmethodservice.InputMethodService#onStartStylusHandwriting", 2905 "android.inputmethodservice.InputMethodService#onFinishStylusHandwriting"}) 2906 public void testNoHandwritingWindow_afterIdleTimeout() throws Exception { 2907 // skip this test if device doesn't have stylus. 2908 assumeTrue("Skipping test on devices that don't stylus connected.", 2909 hasSupportedStylus()); 2910 2911 final InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); 2912 try (MockImeSession imeSession = MockImeSession.create( 2913 InstrumentationRegistry.getInstrumentation().getContext(), 2914 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 2915 new ImeSettings.Builder())) { 2916 final ImeEventStream stream = imeSession.openEventStream(); 2917 2918 final String marker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 2919 final EditText editText = launchTestActivity(marker); 2920 2921 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 2922 notExpectEvent( 2923 stream, 2924 editorMatcher("onStartInputView", marker), 2925 NOT_EXPECT_TIMEOUT); 2926 2927 SystemUtil.runWithShellPermissionIdentity(() -> 2928 imm.setStylusWindowIdleTimeoutForTest(TIMEOUT)); 2929 2930 injectStylusEventToEditorAndVerify(editText, stream, imeSession, 2931 marker, true /* verifyHandwritingStart */, 2932 true /* verifyHandwritingWindowShown */, 2933 false /* verifyHandwritingWindowNotShown */); 2934 2935 // Finish handwriting to remove test stylus id. 2936 imeSession.callFinishStylusHandwriting(); 2937 expectEvent( 2938 stream, 2939 editorMatcher("onFinishStylusHandwriting", marker), 2940 TIMEOUT_1_S); 2941 2942 // Verify handwriting window is removed after stylus handwriting idle-timeout. 2943 TestUtils.waitOnMainUntil(() -> { 2944 try { 2945 // wait until callHasStylusHandwritingWindow returns false 2946 return !expectCommand(stream, imeSession.callHasStylusHandwritingWindow(), 2947 TIMEOUT).getReturnBooleanValue(); 2948 } catch (TimeoutException e) { 2949 e.printStackTrace(); 2950 } 2951 // handwriting window is still around. 2952 return true; 2953 }, TIMEOUT); 2954 2955 // reset idle-timeout 2956 SystemUtil.runWithShellPermissionIdentity(() -> 2957 imm.setStylusWindowIdleTimeoutForTest(0)); 2958 } 2959 } 2960 2961 /** 2962 * Verify that Ink window is around before timeout 2963 */ 2964 @Test 2965 @ApiTest(apis = {"android.view.inputmethod.InputMethodManager#startStylusHandwriting", 2966 "android.inputmethodservice.InputMethodService#onStartStylusHandwriting", 2967 "android.inputmethodservice.InputMethodService#onFinishStylusHandwriting"}) 2968 public void testHandwritingWindow_beforeTimeout() throws Exception { 2969 // skip this test if device doesn't have stylus. 2970 assumeTrue("Skipping test on devices that don't stylus connected.", 2971 hasSupportedStylus()); 2972 2973 final InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); 2974 try (MockImeSession imeSession = MockImeSession.create( 2975 InstrumentationRegistry.getInstrumentation().getContext(), 2976 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 2977 new ImeSettings.Builder())) { 2978 final ImeEventStream stream = imeSession.openEventStream(); 2979 2980 final String marker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 2981 final EditText editText = launchTestActivity(marker); 2982 2983 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 2984 notExpectEvent( 2985 stream, 2986 editorMatcher("onStartInputView", marker), 2987 NOT_EXPECT_TIMEOUT); 2988 2989 SystemUtil.runWithShellPermissionIdentity(() -> 2990 imm.setStylusWindowIdleTimeoutForTest(TIMEOUT)); 2991 2992 injectStylusEventToEditorAndVerify(editText, stream, imeSession, 2993 marker, true /* verifyHandwritingStart */, 2994 true /* verifyHandwritingWindowShown */, 2995 false /* verifyHandwritingWindowNotShown */); 2996 2997 // Finish handwriting to remove test stylus id. 2998 imeSession.callFinishStylusHandwriting(); 2999 expectEvent( 3000 stream, 3001 editorMatcher("onFinishStylusHandwriting", marker), 3002 TIMEOUT_1_S); 3003 3004 // Just any stylus events to delay idle-timeout 3005 TestUtils.injectStylusDownEvent(editText, 0, 0); 3006 TestUtils.injectStylusUpEvent(editText, 0, 0); 3007 3008 // Verify handwriting window is still around as stylus was used recently. 3009 assertTrue(expectCommand( 3010 stream, imeSession.callHasStylusHandwritingWindow(), TIMEOUT_1_S) 3011 .getReturnBooleanValue()); 3012 3013 // Reset idle-timeout 3014 SystemUtil.runWithShellPermissionIdentity(() -> 3015 imm.setStylusWindowIdleTimeoutForTest(0)); 3016 } 3017 } 3018 3019 /** 3020 * Inject stylus events on top of an unfocused custom editor and verify handwriting is started 3021 * and stylus handwriting window is displayed. 3022 */ 3023 @Test 3024 public void testHandwriting_unfocusedCustomEditor() throws Exception { 3025 try (MockImeSession imeSession = MockImeSession.create( 3026 InstrumentationRegistry.getInstrumentation().getContext(), 3027 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 3028 new ImeSettings.Builder())) { 3029 final ImeEventStream stream = imeSession.openEventStream(); 3030 3031 final String focusedMarker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 3032 final String unfocusedMarker = getTestMarker(NON_FOCUSED_EDIT_TEXT_TAG); 3033 final Pair<CustomEditorView, CustomEditorView> customEditorPair = 3034 launchTestActivityWithCustomEditors(focusedMarker, unfocusedMarker); 3035 final CustomEditorView unfocusedCustomEditor = customEditorPair.second; 3036 3037 expectEvent(stream, editorMatcher("onStartInput", focusedMarker), TIMEOUT); 3038 notExpectEvent( 3039 stream, 3040 editorMatcher("onStartInputView", focusedMarker), 3041 NOT_EXPECT_TIMEOUT); 3042 3043 addVirtualStylusIdForTestSession(); 3044 final int touchSlop = getTouchSlop(); 3045 final int startX = unfocusedCustomEditor.getWidth() / 2; 3046 final int startY = unfocusedCustomEditor.getHeight() / 2; 3047 final int endX = startX + 2 * touchSlop; 3048 final int endY = startY + 2 * touchSlop; 3049 final int number = 5; 3050 TestUtils.injectStylusDownEvent(unfocusedCustomEditor, startX, startY); 3051 TestUtils.injectStylusMoveEvents(unfocusedCustomEditor, startX, startY, 3052 endX, endY, number); 3053 try { 3054 // Handwriting should already be initiated before ACTION_UP. 3055 // unfocusedCustomEditor is focused and triggers onStartInput. 3056 expectEvent(stream, editorMatcher("onStartInput", unfocusedMarker), TIMEOUT); 3057 // Keyboard shouldn't show up. 3058 notExpectEvent( 3059 stream, 3060 editorMatcher("onStartInputView", unfocusedMarker), 3061 NOT_EXPECT_TIMEOUT); 3062 // Handwriting should start. 3063 expectEvent( 3064 stream, 3065 editorMatcher("onStartStylusHandwriting", unfocusedMarker), 3066 TIMEOUT); 3067 3068 verifyStylusHandwritingWindowIsShown(stream, imeSession); 3069 3070 // Verify that stylus move events are swallowed by the handwriting initiator once 3071 // handwriting has been initiated and not dispatched to the view tree. 3072 assertThat(unfocusedCustomEditor.mStylusMoveEventCount).isLessThan(number); 3073 } finally { 3074 TestUtils.injectStylusUpEvent(unfocusedCustomEditor, endX, endY); 3075 } 3076 } 3077 } 3078 3079 /** 3080 * Inject stylus events on top of a focused custom editor that disables auto handwriting. 3081 * 3082 * @link InputMethodManager#startStylusHandwriting(View)} should not be called. 3083 */ 3084 @Test 3085 public void testAutoHandwritingDisabled_customEditor() throws Exception { 3086 try (MockImeSession imeSession = MockImeSession.create( 3087 InstrumentationRegistry.getInstrumentation().getContext(), 3088 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 3089 new ImeSettings.Builder())) { 3090 final ImeEventStream stream = imeSession.openEventStream(); 3091 3092 final String focusedMarker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 3093 final String unfocusedMarker = getTestMarker(NON_FOCUSED_EDIT_TEXT_TAG); 3094 final Pair<CustomEditorView, CustomEditorView> customEditorPair = 3095 launchTestActivityWithCustomEditors(focusedMarker, unfocusedMarker); 3096 final CustomEditorView focusedCustomEditor = customEditorPair.first; 3097 focusedCustomEditor.setAutoHandwritingEnabled(false); 3098 3099 expectEvent(stream, editorMatcher("onStartInput", focusedMarker), TIMEOUT); 3100 notExpectEvent( 3101 stream, 3102 editorMatcher("onStartInputView", focusedMarker), 3103 NOT_EXPECT_TIMEOUT); 3104 3105 addVirtualStylusIdForTestSession(); 3106 3107 injectStylusEventToEditorAndVerify( 3108 focusedCustomEditor, stream, imeSession, focusedMarker, 3109 false /* verifyHandwritingStart */, false, 3110 false /* verifyHandwritingWindowIsShown */); 3111 3112 // Verify that all stylus move events are dispatched to the view tree. 3113 assertThat(focusedCustomEditor.mStylusMoveEventCount) 3114 .isEqualTo(NUMBER_OF_INJECTED_EVENTS); 3115 } 3116 } 3117 3118 private void injectStylusEventToEditorAndVerify( 3119 View editor, ImeEventStream stream, MockImeSession imeSession, String marker, 3120 boolean verifyHandwritingStart, boolean verifyHandwritingWindowIsShown, 3121 boolean verifyHandwritingWindowNotShown) throws Exception { 3122 final int touchSlop = getTouchSlop(); 3123 final int startX = editor.getWidth() / 2; 3124 final int startY = editor.getHeight() / 2; 3125 final int endX = startX + 2 * touchSlop; 3126 final int endY = startY + 2 * touchSlop; 3127 TestUtils.injectStylusDownEvent(editor, startX, startY); 3128 TestUtils.injectStylusMoveEvents( 3129 editor, startX, startY, endX, endY, NUMBER_OF_INJECTED_EVENTS); 3130 try { 3131 // Handwriting should already be initiated before ACTION_UP. 3132 // keyboard shouldn't show up. 3133 notExpectEvent( 3134 stream, 3135 editorMatcher("onStartInputView", marker), 3136 NOT_EXPECT_TIMEOUT); 3137 if (verifyHandwritingStart) { 3138 // Handwriting should start 3139 expectEvent( 3140 stream, 3141 editorMatcher("onStartStylusHandwriting", marker), 3142 TIMEOUT); 3143 } else { 3144 // Handwriting should not start 3145 notExpectEvent( 3146 stream, 3147 editorMatcher("onStartStylusHandwriting", marker), 3148 NOT_EXPECT_TIMEOUT); 3149 } 3150 if (verifyHandwritingWindowIsShown) { 3151 verifyStylusHandwritingWindowIsShown(stream, imeSession); 3152 } else if (verifyHandwritingWindowNotShown) { 3153 verifyStylusHandwritingWindowIsNotShown(stream, imeSession); 3154 } 3155 } finally { 3156 TestUtils.injectStylusUpEvent(editor, endX, endY); 3157 } 3158 } 3159 3160 @ApiTest(apis = { 3161 "android.view.inputmethod.InputMethodService#setStylusHandwritingRegion", 3162 }) 3163 @Test 3164 @RequiresFlagsEnabled({FLAG_ADAPTIVE_HANDWRITING_BOUNDS, FLAG_DEVICE_ASSOCIATIONS}) 3165 public void testSetStylusHandwritingRegion() throws Exception { 3166 final InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); 3167 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 3168 try (MockImeSession imeSession = MockImeSession.create( 3169 instrumentation.getContext(), 3170 instrumentation.getUiAutomation(), 3171 new ImeSettings.Builder())) { 3172 final ImeEventStream stream = imeSession.openEventStream(); 3173 3174 final String marker = getTestMarker(FOCUSED_EDIT_TEXT_TAG); 3175 final EditText editText = launchTestActivity(marker); 3176 3177 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 3178 notExpectEvent( 3179 stream, 3180 editorMatcher("onStartInputView", marker), 3181 NOT_EXPECT_TIMEOUT); 3182 3183 addVirtualStylusIdForTestSession(); 3184 // update handwriting window timeout to a larger so that it doesn't timeout 3185 // in-between injections. 3186 SystemUtil.runWithShellPermissionIdentity(() -> 3187 imm.setStylusWindowIdleTimeoutForTest(TIMEOUT * 2)); 3188 3189 final int touchSlop = getTouchSlop(); 3190 int startX = editText.getWidth() / 2; 3191 int startY = editText.getHeight() / 2; 3192 int endX = startX + 2 * touchSlop; 3193 int endY = startY; 3194 final int number = 5; 3195 3196 final Display display = editText.getDisplay(); 3197 try (UinputTouchDevice stylus = new UinputStylus(instrumentation, display)) { 3198 UinputTouchDevice.Pointer pointer = 3199 TestUtils.injectStylusDownEvent(stylus, editText, startX, startY); 3200 TestUtils.injectStylusMoveEvents(pointer, editText, startX, startY, 3201 endX, endY, number); 3202 3203 // Handwriting should already be initiated before ACTION_UP. 3204 // keyboard shouldn't show up. 3205 notExpectEvent( 3206 stream, 3207 editorMatcher("onStartInputView", marker), 3208 NOT_EXPECT_TIMEOUT); 3209 // Handwriting should start 3210 expectEvent( 3211 stream, 3212 editorMatcher("onStartStylusHandwriting", marker), 3213 TIMEOUT); 3214 TestUtils.injectStylusUpEvent(pointer); 3215 3216 // Enlarge the touchable area 3217 final int offset = 100; 3218 endX = editText.getRight() + offset; 3219 endY = editText.getBottom() + offset; 3220 final int endRegionY = endY; 3221 Region hwRegion = new Region(editText.getLeft(), editText.getTop(), endX, endY); 3222 assertTrue(expectCommand( 3223 stream, imeSession.callSetStylusHandwritingRegion(hwRegion), TIMEOUT) 3224 .getReturnBooleanValue()); 3225 3226 // inject motion event within the new expanded touchable region 3227 startX = endX / 2; 3228 endX = startX; 3229 startY = endY - 10; // within handwriting region. 3230 endY = startY + offset; 3231 pointer = TestUtils.injectStylusDownEvent(stylus, startX, startY); 3232 TestUtils.injectStylusMoveEvents(pointer, startX, startY, endX, endY, number); 3233 // verify handwriting is still ongoing 3234 notExpectEvent( 3235 stream, 3236 editorMatcher("onFinishStylusHandwriting", marker), 3237 NOT_EXPECT_TIMEOUT); 3238 TestUtils.injectStylusUpEvent(pointer); 3239 3240 // inject outside touchableRegion, slightly outside endRegionY. 3241 startY = endRegionY + 10; 3242 endY = startY + offset; 3243 pointer = TestUtils.injectStylusDownEvent(stylus, startX, startY); 3244 TestUtils.injectStylusMoveEvents(pointer, startX, startY, endX, endY, number); 3245 // verify finish has been called. 3246 expectEvent( 3247 stream, 3248 editorMatcher("onFinishStylusHandwriting", marker), 3249 TIMEOUT_1_S); 3250 TestUtils.injectStylusUpEvent(pointer); 3251 verifyStylusHandwritingWindowIsNotShown(stream, imeSession); 3252 } 3253 } 3254 } 3255 3256 private EditText launchTestActivity(@NonNull String marker) { 3257 return launchTestActivity(marker, getTestMarker(NON_FOCUSED_EDIT_TEXT_TAG)).first; 3258 } 3259 3260 private static int getTouchSlop() { 3261 final Context context = InstrumentationRegistry.getInstrumentation().getContext(); 3262 // Some tests require stylus movements to exceed the touch slop so that they are not 3263 // interpreted as clicks. Other tests require the movements to exceed the handwriting slop 3264 // to trigger handwriting initiation. Using the larger value allows all tests to pass. 3265 return Math.max( 3266 ViewConfiguration.get(context).getScaledTouchSlop(), 3267 ViewConfiguration.get(context).getScaledHandwritingSlop()); 3268 } 3269 3270 private Pair<EditText, EditText> launchTestActivityNoEditorFocus(@NonNull String focusedMarker, 3271 @NonNull String nonFocusedMarker) { 3272 return launchTestActivity(focusedMarker, nonFocusedMarker, false /* isEditorFocused */); 3273 } 3274 3275 private Pair<EditText, EditText> launchTestActivity(@NonNull String focusedMarker, 3276 @NonNull String nonFocusedMarker) { 3277 return launchTestActivity(focusedMarker, nonFocusedMarker, true /* isEditorFocused */); 3278 } 3279 3280 private Pair<EditText, EditText> launchTestActivity(@NonNull String focusedMarker, 3281 @NonNull String nonFocusedMarker, boolean isEditorFocused) { 3282 final AtomicReference<EditText> focusedEditTextRef = new AtomicReference<>(); 3283 final AtomicReference<EditText> nonFocusedEditTextRef = new AtomicReference<>(); 3284 TestActivity.startSync(activity -> { 3285 final LinearLayout layout = new LinearLayout(activity); 3286 layout.setOrientation(LinearLayout.VERTICAL); 3287 // Adding some top padding tests that inject stylus event out of the view boundary. 3288 layout.setPadding(0, 100, 0, 0); 3289 3290 final EditText focusedEditText = new EditText(activity); 3291 focusedEditText.setHint("focused editText"); 3292 focusedEditText.setPrivateImeOptions(focusedMarker); 3293 if (isEditorFocused) { 3294 focusedEditText.requestFocus(); 3295 } 3296 focusedEditText.setAutoHandwritingEnabled(true); 3297 focusedEditText.setHandwritingBoundsOffsets( 3298 HANDWRITING_BOUNDS_OFFSET_PX, 3299 HANDWRITING_BOUNDS_OFFSET_PX, 3300 HANDWRITING_BOUNDS_OFFSET_PX, 3301 HANDWRITING_BOUNDS_OFFSET_PX); 3302 focusedEditTextRef.set(focusedEditText); 3303 layout.addView(focusedEditText); 3304 3305 final EditText nonFocusedEditText = new EditText(activity); 3306 nonFocusedEditText.setPrivateImeOptions(nonFocusedMarker); 3307 nonFocusedEditText.setHint("target editText"); 3308 nonFocusedEditText.setAutoHandwritingEnabled(true); 3309 nonFocusedEditTextRef.set(nonFocusedEditText); 3310 nonFocusedEditText.setHandwritingBoundsOffsets( 3311 HANDWRITING_BOUNDS_OFFSET_PX, 3312 HANDWRITING_BOUNDS_OFFSET_PX, 3313 HANDWRITING_BOUNDS_OFFSET_PX, 3314 HANDWRITING_BOUNDS_OFFSET_PX); 3315 // Leave margin between the EditTexts so that their extended handwriting bounds do not 3316 // overlap. 3317 LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( 3318 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); 3319 layoutParams.topMargin = 3 * HANDWRITING_BOUNDS_OFFSET_PX; 3320 layout.addView(nonFocusedEditText, layoutParams); 3321 return layout; 3322 }); 3323 return new Pair<>(focusedEditTextRef.get(), nonFocusedEditTextRef.get()); 3324 } 3325 3326 private Pair<CustomEditorView, CustomEditorView> launchTestActivityWithCustomEditors( 3327 @NonNull String focusedMarker, @NonNull String unfocusedMarker) { 3328 final AtomicReference<CustomEditorView> focusedCustomEditorRef = new AtomicReference<>(); 3329 final AtomicReference<CustomEditorView> unfocusedCustomEditorRef = new AtomicReference<>(); 3330 TestActivity.startSync(activity -> { 3331 final LinearLayout layout = new LinearLayout(activity); 3332 layout.setOrientation(LinearLayout.VERTICAL); 3333 // Add some top padding for tests that inject stylus event out of the view boundary. 3334 layout.setPadding(0, 100, 0, 0); 3335 3336 final CustomEditorView focusedCustomEditor = 3337 new CustomEditorView(activity, focusedMarker, Color.RED); 3338 focusedCustomEditor.setAutoHandwritingEnabled(true); 3339 focusedCustomEditor.requestFocus(); 3340 focusedCustomEditorRef.set(focusedCustomEditor); 3341 layout.addView(focusedCustomEditor); 3342 3343 final CustomEditorView unfocusedCustomEditor = 3344 new CustomEditorView(activity, unfocusedMarker, Color.BLUE); 3345 unfocusedCustomEditor.setAutoHandwritingEnabled(true); 3346 unfocusedCustomEditorRef.set(unfocusedCustomEditor); 3347 layout.addView(unfocusedCustomEditor); 3348 3349 return layout; 3350 }); 3351 return new Pair<>(focusedCustomEditorRef.get(), unfocusedCustomEditorRef.get()); 3352 } 3353 3354 private View launchTestActivityWithDelegate( 3355 @NonNull String editTextMarker, CountDownLatch delegateLatch, long delegateDelayMs) { 3356 final AtomicReference<View> delegatorViewRef = new AtomicReference<>(); 3357 TestActivity.startSync(activity -> { 3358 final LinearLayout layout = new LinearLayout(activity); 3359 layout.setOrientation(LinearLayout.VERTICAL); 3360 3361 final View delegatorView = new View(activity); 3362 delegatorViewRef.set(delegatorView); 3363 delegatorView.setBackgroundColor(Color.GREEN); 3364 delegatorView.setHandwritingDelegatorCallback( 3365 () -> { 3366 final EditText editText = new EditText(activity); 3367 editText.setIsHandwritingDelegate(true); 3368 editText.setPrivateImeOptions(editTextMarker); 3369 editText.setHint("editText"); 3370 editText.setId(R.id.handwriting_delegate); 3371 layout.addView(editText); 3372 editText.postDelayed(() -> { 3373 editText.requestFocus(); 3374 if (delegateLatch != null) { 3375 delegateLatch.countDown(); 3376 } 3377 }, delegateDelayMs); 3378 }); 3379 3380 LinearLayout.LayoutParams layoutParams = 3381 new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 40); 3382 // Add space so that stylus motion on the delegate view is not within the edit text's 3383 // extended handwriting bounds. 3384 layoutParams.bottomMargin = 200; 3385 layout.addView(delegatorView, layoutParams); 3386 return layout; 3387 }); 3388 return delegatorViewRef.get(); 3389 } 3390 3391 private View launchTestActivityInExternalPackage( 3392 @NonNull final String editTextMarker, final boolean setAllowedDelegatorPackage) { 3393 final AtomicReference<View> delegateViewRef = new AtomicReference<>(); 3394 TestActivity.startSync(activity -> { 3395 final LinearLayout layout = new LinearLayout(activity); 3396 layout.setOrientation(LinearLayout.VERTICAL); 3397 3398 final View delegatorView = new View(activity); 3399 delegateViewRef.set(delegatorView); 3400 delegatorView.setBackgroundColor(Color.GREEN); 3401 3402 delegatorView.setHandwritingDelegatorCallback(()-> { 3403 // launch activity in a different package. 3404 Intent intent = new Intent(Intent.ACTION_MAIN); 3405 intent.setComponent(new ComponentName( 3406 "android.view.inputmethod.ctstestapp", 3407 "android.view.inputmethod.ctstestapp.MainActivity")); 3408 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 3409 intent.putExtra(MockTestActivityUtil.EXTRA_KEY_PRIVATE_IME_OPTIONS, editTextMarker); 3410 intent.putExtra(MockTestActivityUtil.EXTRA_HANDWRITING_DELEGATE, true); 3411 activity.startActivity(intent); 3412 }); 3413 if (setAllowedDelegatorPackage) { 3414 delegatorView.setAllowedHandwritingDelegatePackage( 3415 "android.view.inputmethod.ctstestapp"); 3416 } 3417 layout.addView( 3418 delegatorView, 3419 new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 40)); 3420 return layout; 3421 }); 3422 return delegateViewRef.get(); 3423 } 3424 3425 private boolean hasSupportedStylus() { 3426 final InputManager im = mContext.getSystemService(InputManager.class); 3427 for (int id : im.getInputDeviceIds()) { 3428 InputDevice inputDevice = im.getInputDevice(id); 3429 if (inputDevice != null && isStylusDevice(inputDevice)) { 3430 return true; 3431 } 3432 } 3433 return false; 3434 } 3435 3436 private static boolean isStylusDevice(InputDevice inputDevice) { 3437 return inputDevice.supportsSource(InputDevice.SOURCE_STYLUS) 3438 || inputDevice.supportsSource(InputDevice.SOURCE_BLUETOOTH_STYLUS); 3439 } 3440 3441 private void addVirtualStylusIdForTestSession() { 3442 SystemUtil.runWithShellPermissionIdentity(() -> { 3443 mContext.getSystemService(InputMethodManager.class) 3444 .addVirtualStylusIdForTestSession(); 3445 }, Manifest.permission.TEST_INPUT_METHOD); 3446 } 3447 3448 private String getDefaultLauncher() throws Exception { 3449 final String prefix = "Launcher: ComponentInfo{"; 3450 final String postfix = "}"; 3451 for (String s : 3452 SystemUtil.runShellCommand("cmd shortcut get-default-launcher").split("\n")) { 3453 if (s.startsWith(prefix) && s.endsWith(postfix)) { 3454 return s.substring(prefix.length(), s.length() - postfix.length()); 3455 } 3456 } 3457 throw new Exception("Default launcher not found"); 3458 } 3459 3460 private void setDefaultLauncher(String component) { 3461 SystemUtil.runShellCommand("cmd package set-home-activity " + component); 3462 } 3463 3464 private static final class CustomEditorView extends View { 3465 private final String mMarker; 3466 private int mStylusMoveEventCount = 0; 3467 3468 private CustomEditorView(Context context, @NonNull String marker, 3469 @ColorInt int backgroundColor) { 3470 super(context); 3471 mMarker = marker; 3472 setFocusable(true); 3473 setFocusableInTouchMode(true); 3474 setBackgroundColor(backgroundColor); 3475 } 3476 3477 @Override 3478 public boolean onCheckIsTextEditor() { 3479 return true; 3480 } 3481 3482 @Override 3483 public InputConnection onCreateInputConnection(EditorInfo outAttrs) { 3484 outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT; 3485 outAttrs.privateImeOptions = mMarker; 3486 return new NoOpInputConnection(); 3487 } 3488 3489 @Override 3490 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 3491 // This View needs a valid size to be focusable. 3492 setMeasuredDimension(300, 100); 3493 } 3494 3495 @Override 3496 public boolean onTouchEvent(MotionEvent event) { 3497 if (event.getToolType(event.getActionIndex()) == MotionEvent.TOOL_TYPE_STYLUS) { 3498 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 3499 // Return true to receive ACTION_MOVE events. 3500 return true; 3501 } else if (event.getActionMasked() == MotionEvent.ACTION_MOVE) { 3502 mStylusMoveEventCount++; 3503 } 3504 } 3505 return super.onTouchEvent(event); 3506 } 3507 } 3508 3509 private static final class TestCallback implements ConnectionlessHandwritingCallback { 3510 private CharSequence mResultText; 3511 public int mErrorCode = -1; 3512 3513 @Override 3514 public void onResult(@NonNull CharSequence text) { 3515 assertNoCallbackMethodsPreviouslyCalled(); 3516 mResultText = text; 3517 } 3518 3519 @Override 3520 public void onError(int errorCode) { 3521 assertNoCallbackMethodsPreviouslyCalled(); 3522 mErrorCode = errorCode; 3523 } 3524 3525 // Used to verify that the callback only receives a single result. 3526 private void assertNoCallbackMethodsPreviouslyCalled() { 3527 assertThat(mResultText).isNull(); 3528 assertThat(mErrorCode).isEqualTo(-1); 3529 } 3530 } 3531 } 3532