• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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