| /* |
| * Copyright (C) 2021 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.view.inputmethod.cts; |
| |
| import static android.provider.Settings.Secure.STYLUS_HANDWRITING_DEFAULT_VALUE; |
| import static android.provider.Settings.Secure.STYLUS_HANDWRITING_ENABLED; |
| import static android.view.inputmethod.Flags.FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR; |
| import static android.view.inputmethod.InputMethodInfo.ACTION_STYLUS_HANDWRITING_SETTINGS; |
| |
| import static com.android.cts.mockime.ImeEventStreamTestUtils.editorMatcher; |
| import static com.android.cts.mockime.ImeEventStreamTestUtils.expectBindInput; |
| import static com.android.cts.mockime.ImeEventStreamTestUtils.expectCommand; |
| import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent; |
| import static com.android.cts.mockime.ImeEventStreamTestUtils.notExpectEvent; |
| import static com.android.cts.mockime.ImeEventStreamTestUtils.withDescription; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assume.assumeFalse; |
| import static org.junit.Assume.assumeTrue; |
| import static org.mockito.Mockito.mock; |
| |
| import android.Manifest; |
| import android.app.Activity; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.PackageManager; |
| import android.graphics.Color; |
| import android.hardware.input.InputManager; |
| import android.inputmethodservice.InputMethodService; |
| import android.os.Process; |
| import android.os.SystemClock; |
| import android.platform.test.annotations.AppModeFull; |
| import android.platform.test.annotations.AppModeSdkSandbox; |
| import android.platform.test.annotations.RequiresFlagsEnabled; |
| import android.platform.test.flag.junit.CheckFlagsRule; |
| import android.platform.test.flag.junit.DeviceFlagsValueProvider; |
| import android.provider.Settings; |
| import android.text.TextUtils; |
| import android.util.DisplayMetrics; |
| import android.util.Pair; |
| import android.view.InputDevice; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewConfiguration; |
| import android.view.ViewGroup; |
| import android.view.inputmethod.EditorInfo; |
| import android.view.inputmethod.InputConnection; |
| import android.view.inputmethod.InputMethodInfo; |
| import android.view.inputmethod.InputMethodManager; |
| import android.view.inputmethod.cts.util.EndToEndImeTestBase; |
| import android.view.inputmethod.cts.util.MockTestActivityUtil; |
| import android.view.inputmethod.cts.util.NoOpInputConnection; |
| import android.view.inputmethod.cts.util.TestActivity; |
| import android.view.inputmethod.cts.util.TestActivity2; |
| import android.view.inputmethod.cts.util.TestUtils; |
| import android.widget.EditText; |
| import android.widget.LinearLayout; |
| |
| import androidx.annotation.ColorInt; |
| import androidx.annotation.NonNull; |
| import androidx.test.filters.FlakyTest; |
| import androidx.test.platform.app.InstrumentationRegistry; |
| |
| import com.android.compatibility.common.util.ApiTest; |
| import com.android.compatibility.common.util.CommonTestUtils; |
| import com.android.compatibility.common.util.GestureNavSwitchHelper; |
| import com.android.compatibility.common.util.SystemUtil; |
| import com.android.cts.mockime.ImeEvent; |
| import com.android.cts.mockime.ImeEventStream; |
| import com.android.cts.mockime.ImeSettings; |
| import com.android.cts.mockime.MockImeSession; |
| |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| |
| import java.util.ArrayList; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.TimeoutException; |
| import java.util.concurrent.atomic.AtomicReference; |
| import java.util.function.Predicate; |
| |
| /** |
| * IMF and end-to-end Stylus handwriting tests. |
| */ |
| @AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).") |
| public class StylusHandwritingTest extends EndToEndImeTestBase { |
| private static final long TIMEOUT_IN_SECONDS = 5; |
| private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(TIMEOUT_IN_SECONDS); |
| private static final long TIMEOUT_6_S = TimeUnit.SECONDS.toMillis(6); |
| private static final long TIMEOUT_1_S = TimeUnit.SECONDS.toMillis(1); |
| private static final long NOT_EXPECT_TIMEOUT_IN_SECONDS = 3; |
| private static final long NOT_EXPECT_TIMEOUT = |
| TimeUnit.SECONDS.toMillis(NOT_EXPECT_TIMEOUT_IN_SECONDS); |
| private static final int SETTING_VALUE_ON = 1; |
| private static final int SETTING_VALUE_OFF = 0; |
| private static final String TEST_MARKER_PREFIX = |
| "android.view.inputmethod.cts.StylusHandwritingTest"; |
| private static final int HANDWRITING_BOUNDS_OFFSET_PX = 20; |
| // A timeout greater than HandwritingModeController#HANDWRITING_DELEGATION_IDLE_TIMEOUT_MS. |
| private static final long DELEGATION_AFTER_IDLE_TIMEOUT_MS = 3100; |
| private static final int NUMBER_OF_INJECTED_EVENTS = 5; |
| private static final String TEST_LAUNCHER_COMPONENT = |
| "android.view.inputmethod.ctstestlauncher/" |
| + "android.view.inputmethod.ctstestlauncher.LauncherActivity"; |
| |
| private Context mContext; |
| private int mHwInitialState; |
| private boolean mShouldRestoreInitialHwState; |
| private String mDefaultLauncherToRestore; |
| |
| private static final GestureNavSwitchHelper sGestureNavRule = new GestureNavSwitchHelper(); |
| |
| @Rule |
| public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); |
| |
| @Before |
| public void setup() { |
| mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); |
| assumeFalse(mContext.getPackageManager().hasSystemFeature( |
| PackageManager.FEATURE_LEANBACK_ONLY)); |
| assumeFalse(mContext.getPackageManager().hasSystemFeature( |
| PackageManager.FEATURE_AUTOMOTIVE)); |
| |
| mHwInitialState = Settings.Secure.getInt(mContext.getContentResolver(), |
| STYLUS_HANDWRITING_ENABLED, STYLUS_HANDWRITING_DEFAULT_VALUE); |
| if (mHwInitialState != SETTING_VALUE_ON) { |
| SystemUtil.runWithShellPermissionIdentity(() -> { |
| Settings.Secure.putInt(mContext.getContentResolver(), |
| STYLUS_HANDWRITING_ENABLED, SETTING_VALUE_ON); |
| }, Manifest.permission.WRITE_SECURE_SETTINGS); |
| mShouldRestoreInitialHwState = true; |
| } |
| } |
| |
| @After |
| public void tearDown() { |
| MockTestActivityUtil.forceStopPackage(); |
| if (mShouldRestoreInitialHwState) { |
| mShouldRestoreInitialHwState = false; |
| SystemUtil.runWithShellPermissionIdentity(() -> { |
| Settings.Secure.putInt(mContext.getContentResolver(), |
| STYLUS_HANDWRITING_ENABLED, mHwInitialState); |
| }, Manifest.permission.WRITE_SECURE_SETTINGS); |
| } |
| if (mDefaultLauncherToRestore != null) { |
| setDefaultLauncher(mDefaultLauncherToRestore); |
| mDefaultLauncherToRestore = null; |
| } |
| } |
| |
| /** |
| * Verify current IME has {@link InputMethodInfo} for stylus handwriting, settings. |
| */ |
| @Test |
| @ApiTest(apis = {"android.view.inputmethod.InputMethodInfo#supportsStylusHandwriting", |
| "android.view.inputmethod.InputMethodInfo#ACTION_STYLUS_HANDWRITING_SETTINGS", |
| "android.view.inputmethod.InputMethodInfo#createStylusHandwritingSettingsActivityIntent" |
| }) |
| public void testHandwritingInfo() throws Exception { |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| InputMethodInfo info = imeSession.getInputMethodInfo(); |
| assertTrue(info.supportsStylusHandwriting()); |
| // TODO(b/217957587): migrate CtsMockInputMethodLib to android_library and use |
| // string resource. |
| Intent stylusSettingsIntent = info.createStylusHandwritingSettingsActivityIntent(); |
| assertEquals(ACTION_STYLUS_HANDWRITING_SETTINGS, stylusSettingsIntent.getAction()); |
| assertEquals("handwriting_settings", |
| stylusSettingsIntent.getComponent().getClassName()); |
| } |
| } |
| |
| @Test |
| public void testIsStylusHandwritingAvailable_prefDisabled() throws Exception { |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| imeSession.openEventStream(); |
| |
| // Disable pref |
| SystemUtil.runWithShellPermissionIdentity(() -> { |
| Settings.Secure.putInt(mContext.getContentResolver(), |
| STYLUS_HANDWRITING_ENABLED, SETTING_VALUE_OFF); |
| }, Manifest.permission.WRITE_SECURE_SETTINGS); |
| mShouldRestoreInitialHwState = true; |
| |
| launchTestActivity(getTestMarker()); |
| assertFalse( |
| "should return false for isStylusHandwritingAvailable() when pref is disabled", |
| mContext.getSystemService( |
| InputMethodManager.class).isStylusHandwritingAvailable()); |
| } |
| } |
| |
| @Test |
| public void testIsStylusHandwritingAvailable() throws Exception { |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| imeSession.openEventStream(); |
| |
| launchTestActivity(getTestMarker()); |
| assertTrue("Mock IME should return true for isStylusHandwritingAvailable() ", |
| mContext.getSystemService( |
| InputMethodManager.class).isStylusHandwritingAvailable()); |
| } |
| } |
| |
| /** |
| * Test to verify that we dont init handwriting on devices that dont have any supported stylus. |
| */ |
| @Test |
| public void testHandwritingNoInitOnDeviceWithNoStylus() { |
| assumeTrue("Skipping test on devices that do not have stylus support", |
| hasSupportedStylus()); |
| final InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| final String marker = getTestMarker(); |
| final EditText editText = launchTestActivity(marker); |
| imm.startStylusHandwriting(editText); |
| // Handwriting should not start since there are no stylus devices registered. |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartStylusHandwriting", marker), |
| NOT_EXPECT_TIMEOUT); |
| } catch (Exception e) { |
| } |
| } |
| |
| @Test |
| public void testHandwritingDoesNotStartWhenNoStylusDown() throws Exception { |
| final InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String marker = getTestMarker(); |
| final EditText editText = launchTestActivity(marker); |
| |
| expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", marker), |
| NOT_EXPECT_TIMEOUT); |
| |
| addVirtualStylusIdForTestSession(); |
| imm.startStylusHandwriting(editText); |
| |
| // Handwriting should not start |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartStylusHandwriting", marker), |
| NOT_EXPECT_TIMEOUT); |
| |
| verifyStylusHandwritingWindowIsNotShown(stream, imeSession); |
| } |
| } |
| |
| @Test |
| public void testHandwritingStartAndFinish() throws Exception { |
| final InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String marker = getTestMarker(); |
| final EditText editText = launchTestActivity(marker); |
| |
| expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", marker), |
| NOT_EXPECT_TIMEOUT); |
| |
| addVirtualStylusIdForTestSession(); |
| // Touch down with a stylus |
| final int startX = editText.getWidth() / 2; |
| final int startY = editText.getHeight() / 2; |
| TestUtils.injectStylusDownEvent(editText, startX, startY); |
| |
| imm.startStylusHandwriting(editText); |
| // keyboard shouldn't show up. |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", marker), |
| NOT_EXPECT_TIMEOUT); |
| |
| // Handwriting should start |
| expectEvent( |
| stream, |
| editorMatcher("onPrepareStylusHandwriting", marker), |
| TIMEOUT); |
| expectEvent( |
| stream, |
| editorMatcher("onStartStylusHandwriting", marker), |
| TIMEOUT); |
| |
| verifyStylusHandwritingWindowIsShown(stream, imeSession); |
| |
| // Release the stylus pointer |
| TestUtils.injectStylusUpEvent(editText, startX, startY); |
| |
| // Verify calling finishStylusHandwriting() calls onFinishStylusHandwriting(). |
| imeSession.callFinishStylusHandwriting(); |
| expectEvent( |
| stream, |
| editorMatcher("onFinishStylusHandwriting", marker), |
| TIMEOUT); |
| } |
| } |
| |
| /** |
| * Verifies that stylus hover events initializes the InkWindow. |
| * @throws Exception |
| */ |
| @Test |
| public void testStylusHoverInitInkWindow() throws Exception { |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String marker = getTestMarker(); |
| final EditText editText = launchTestActivity(marker); |
| |
| expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", marker), |
| NOT_EXPECT_TIMEOUT); |
| |
| addVirtualStylusIdForTestSession(); |
| // Verify there is no handwriting window before stylus is added. |
| assertFalse(expectCommand( |
| stream, imeSession.callHasStylusHandwritingWindow(), TIMEOUT_1_S) |
| .getReturnBooleanValue()); |
| // Stylus hover |
| final int startX = editText.getWidth() / 2; |
| final int startY = editText.getHeight() / 2; |
| TestUtils.injectStylusHoverEvents(editText, startX, startY); |
| // keyboard shouldn't show up. |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", marker), |
| NOT_EXPECT_TIMEOUT); |
| |
| // Handwriting prep should start for stylus onHover |
| expectEvent( |
| stream, |
| editorMatcher("onPrepareStylusHandwriting", marker), |
| TIMEOUT); |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartStylusHandwriting", marker), |
| NOT_EXPECT_TIMEOUT); |
| |
| // Verify handwriting window exists but not shown. |
| assertTrue(expectCommand( |
| stream, imeSession.callHasStylusHandwritingWindow(), TIMEOUT_1_S) |
| .getReturnBooleanValue()); |
| verifyStylusHandwritingWindowIsNotShown(stream, imeSession); |
| } |
| } |
| |
| /** |
| * Call {@link InputMethodManager#startStylusHandwriting(View)} and inject Stylus touch events |
| * on screen. Make sure {@link InputMethodService#onStylusHandwritingMotionEvent(MotionEvent)} |
| * receives those events via Spy window surface. |
| * @throws Exception |
| */ |
| @Test |
| public void testHandwritingStylusEvents_onStylusHandwritingMotionEvent() throws Exception { |
| testHandwritingStylusEvents(false /* verifyOnInkView */); |
| } |
| |
| /** |
| * Call {@link InputMethodManager#startStylusHandwriting(View)} and inject Stylus touch events |
| * on screen. Make sure Inking view receives those events via Spy window surface. |
| * @throws Exception |
| */ |
| @Test |
| public void testHandwritingStylusEvents_dispatchToInkView() throws Exception { |
| testHandwritingStylusEvents(false /* verifyOnInkView */); |
| } |
| |
| private void verifyStylusHandwritingWindowIsShown(ImeEventStream stream, |
| MockImeSession imeSession) throws InterruptedException, TimeoutException { |
| CommonTestUtils.waitUntil("Stylus handwriting window should be shown", TIMEOUT_IN_SECONDS, |
| () -> expectCommand( |
| stream, imeSession.callGetStylusHandwritingWindowVisibility(), TIMEOUT) |
| .getReturnBooleanValue()); |
| } |
| |
| private void verifyStylusHandwritingWindowIsNotShown(ImeEventStream stream, |
| MockImeSession imeSession) throws InterruptedException, TimeoutException { |
| CommonTestUtils.waitUntil("Stylus handwriting window should not be shown", |
| NOT_EXPECT_TIMEOUT_IN_SECONDS, |
| () -> !expectCommand( |
| stream, imeSession.callGetStylusHandwritingWindowVisibility(), TIMEOUT) |
| .getReturnBooleanValue()); |
| } |
| |
| private void testHandwritingStylusEvents(boolean verifyOnInkView) throws Exception { |
| final InputMethodManager imm = InstrumentationRegistry.getInstrumentation() |
| .getTargetContext().getSystemService(InputMethodManager.class); |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String marker = getTestMarker(); |
| final EditText editText = launchTestActivity(marker); |
| |
| expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", marker), |
| NOT_EXPECT_TIMEOUT); |
| |
| addVirtualStylusIdForTestSession(); |
| final List<MotionEvent> injectedEvents = new ArrayList<>(); |
| // Touch down with a stylus |
| final int startX = editText.getWidth() / 2; |
| final int startY = editText.getHeight() / 2; |
| injectedEvents.add(TestUtils.injectStylusDownEvent(editText, startX, startY)); |
| imm.startStylusHandwriting(editText); |
| |
| // Handwriting should start |
| expectEvent( |
| stream, |
| editorMatcher("onStartStylusHandwriting", marker), |
| TIMEOUT); |
| |
| verifyStylusHandwritingWindowIsShown(stream, imeSession); |
| |
| if (verifyOnInkView) { |
| // Set IME stylus Ink view |
| assertTrue(expectCommand( |
| stream, |
| imeSession.callSetStylusHandwritingInkView(), |
| TIMEOUT).getReturnBooleanValue()); |
| } |
| |
| final int touchSlop = getTouchSlop(); |
| |
| final int endX = startX + 2 * touchSlop; |
| final int endY = startY; |
| final int number = 5; |
| |
| injectedEvents.addAll( |
| TestUtils.injectStylusMoveEvents(editText, startX, startY, endX, endY, number)); |
| injectedEvents.add(TestUtils.injectStylusUpEvent(editText, endX, endY)); |
| |
| expectEvent( |
| stream, event -> "onStylusMotionEvent".equals(event.getEventName()), TIMEOUT); |
| |
| // get Stylus events from Ink view, splitting any batched events. |
| final ArrayList<MotionEvent> capturedBatchedEvents = expectCommand( |
| stream, imeSession.callGetStylusHandwritingEvents(), TIMEOUT) |
| .getReturnParcelableArrayListValue(); |
| assertNotNull(capturedBatchedEvents); |
| final ArrayList<MotionEvent> capturedEvents = new ArrayList<>(); |
| capturedBatchedEvents.forEach( |
| e -> capturedEvents.addAll(TestUtils.splitBatchedMotionEvent(e))); |
| |
| // captured events should be same as injected. |
| assertEquals(injectedEvents.size(), capturedEvents.size()); |
| |
| // Verify MotionEvents as well. |
| // Note: we cannot just use equals() since some MotionEvent fields can change after |
| // dispatch. |
| Iterator<MotionEvent> capturedIt = capturedEvents.iterator(); |
| Iterator<MotionEvent> injectedIt = injectedEvents.iterator(); |
| while (injectedIt.hasNext() && capturedIt.hasNext()) { |
| MotionEvent injected = injectedIt.next(); |
| MotionEvent captured = capturedIt.next(); |
| assertEquals("X should be same for MotionEvent", injected.getX(), captured.getX(), |
| 5.0f); |
| assertEquals("Y should be same for MotionEvent", injected.getY(), captured.getY(), |
| 5.0f); |
| assertEquals("Action should be same for MotionEvent", |
| injected.getAction(), captured.getAction()); |
| } |
| } |
| } |
| |
| @FlakyTest(bugId = 210039666) |
| @Test |
| /** |
| * Inject Stylus events on top of focused editor and verify Handwriting is started and InkWindow |
| * is displayed. |
| */ |
| public void testHandwritingEndToEnd() throws Exception { |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String marker = getTestMarker(); |
| final EditText editText = launchTestActivity(marker); |
| |
| expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", marker), |
| NOT_EXPECT_TIMEOUT); |
| |
| addVirtualStylusIdForTestSession(); |
| |
| injectStylusEventToEditorAndVerify(editText, stream, imeSession, marker, |
| true /* verifyHandwritingStart */, true /* verifyHandwritingWindowShown */, |
| false /* verifyHandwritingWindowNotShown */); |
| } |
| } |
| |
| @FlakyTest(bugId = 222840964) |
| @Test |
| /** |
| * Inject Stylus events on top of focused editor and verify Handwriting can be initiated |
| * multiple times. |
| */ |
| public void testHandwritingInitMultipleTimes() throws Exception { |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String marker = getTestMarker(); |
| final EditText editText = launchTestActivity(marker); |
| |
| expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", marker), |
| NOT_EXPECT_TIMEOUT); |
| |
| final int touchSlop = getTouchSlop(); |
| final int startX = editText.getWidth() / 2; |
| final int startY = editText.getHeight() / 2; |
| final int endX = startX + 2 * touchSlop; |
| final int endY = startY; |
| final int number = 5; |
| |
| // Try to init handwriting for multiple times. |
| for (int i = 0; i < 3; ++i) { |
| addVirtualStylusIdForTestSession(); |
| |
| injectStylusEventToEditorAndVerify(editText, stream, imeSession, marker, |
| true /* verifyHandwritingStart */, true /* verifyHandwritingWindowShown */, |
| false /* verifyHandwritingWindowNotShown */); |
| |
| imeSession.callFinishStylusHandwriting(); |
| expectEvent( |
| stream, |
| editorMatcher("onFinishStylusHandwriting", marker), |
| TIMEOUT); |
| } |
| } |
| } |
| |
| @Test |
| /** |
| * Inject Stylus events on top of focused editor's handwriting bounds and verify |
| * Handwriting is started and InkWindow is displayed. |
| */ |
| public void testHandwritingInOffsetHandwritingBounds() throws Exception { |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String marker = getTestMarker(); |
| final EditText editText = launchTestActivity(marker); |
| |
| expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", marker), |
| NOT_EXPECT_TIMEOUT); |
| |
| addVirtualStylusIdForTestSession(); |
| injectStylusEventToEditorAndVerify(editText, stream, imeSession, marker, |
| true /* verifyHandwritingStart */, true /* verifyHandwritingWindowShown */, |
| false /* verifyHandwritingWindowNotShown */); |
| } |
| } |
| |
| /** |
| * Inject Stylus events on top of focused editor and verify Handwriting is started and then |
| * inject events on navbar to swipe to home and make sure motionEvents are consumed by |
| * Handwriting window. |
| */ |
| @Test |
| public void testStylusSession_stylusWouldNotTriggerNavbarGestures() throws Exception { |
| assumeTrue(sGestureNavRule.isGestureMode()); |
| |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String marker = getTestMarker(); |
| final EditText editText = launchTestActivity(marker); |
| |
| expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", marker), |
| NOT_EXPECT_TIMEOUT); |
| |
| addVirtualStylusIdForTestSession(); |
| |
| injectStylusEventToEditorAndVerify(editText, stream, imeSession, marker, |
| true /* verifyHandwritingStart */, |
| false /* verifyHandwritingWindowShown */, |
| false /* verifyHandwritingWindowNotShown */); |
| |
| // Inject stylus swipe up on navbar. |
| TestUtils.injectNavBarToHomeGestureEvents( |
| ((Activity) editText.getContext()), MotionEvent.TOOL_TYPE_STYLUS); |
| |
| // Handwriting is finished if navigation gesture is executed. |
| // Make sure handwriting isn't finished. |
| notExpectEvent( |
| stream, |
| editorMatcher("onFinishStylusHandwriting", marker), |
| TIMEOUT_1_S); |
| } |
| } |
| |
| /** |
| * Inject Stylus events on top of focused editor and verify Handwriting is started and then |
| * inject finger touch events on navbar to swipe to home and make sure user can swipe to home. |
| */ |
| @Test |
| public void testStylusSession_fingerTriggersNavbarGestures() throws Exception { |
| assumeTrue(sGestureNavRule.isGestureMode()); |
| |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String marker = getTestMarker(); |
| final EditText editText = launchTestActivity(marker); |
| |
| expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", marker), |
| NOT_EXPECT_TIMEOUT); |
| |
| addVirtualStylusIdForTestSession(); |
| injectStylusEventToEditorAndVerify(editText, stream, imeSession, marker, |
| true /* verifyHandwritingStart */, |
| false /* verifyHandwritingWindowShown */, |
| false /* verifyHandwritingWindowNotShown */); |
| |
| // Inject finger swipe up on navbar. |
| TestUtils.injectNavBarToHomeGestureEvents( |
| ((Activity) editText.getContext()), MotionEvent.TOOL_TYPE_FINGER); |
| |
| // Handwriting is finished if navigation gesture is executed. |
| // Make sure handwriting is finished to ensure swipe to home works. |
| expectEvent( |
| stream, |
| editorMatcher("onFinishStylusHandwriting", marker), |
| // BlastSyncEngine has a 5s timeout when launcher fails to sync its |
| // transaction, exceeding it avoids flakes when that happens. |
| TIMEOUT_6_S); |
| } |
| } |
| |
| @Test |
| /** |
| * Inject stylus events to a focused EditText that disables autoHandwriting. |
| * {@link InputMethodManager#startStylusHandwriting(View)} should not be called. |
| */ |
| public void testAutoHandwritingDisabled() throws Exception { |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String marker = getTestMarker(); |
| final EditText editText = launchTestActivity(marker); |
| editText.setAutoHandwritingEnabled(false); |
| |
| expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", marker), |
| NOT_EXPECT_TIMEOUT); |
| |
| addVirtualStylusIdForTestSession(); |
| TestUtils.injectStylusEvents(editText); |
| |
| // TODO(215439842): check that keyboard is not shown. |
| // Handwriting should not start |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartStylusHandwriting", marker), |
| NOT_EXPECT_TIMEOUT); |
| |
| verifyStylusHandwritingWindowIsNotShown(stream, imeSession); |
| } |
| } |
| |
| @Test |
| /** |
| * Inject stylus events out of a focused editor's view bound. |
| * {@link InputMethodManager#startStylusHandwriting(View)} should not be called for this editor. |
| */ |
| public void testAutoHandwritingOutOfBound() throws Exception { |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String marker = getTestMarker(); |
| final EditText editText = launchTestActivity(marker); |
| editText.setAutoHandwritingEnabled(false); |
| |
| expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", marker), |
| NOT_EXPECT_TIMEOUT); |
| |
| addVirtualStylusIdForTestSession(); |
| // Inject stylus events out of the editor boundary. |
| TestUtils.injectStylusEvents(editText, editText.getWidth() / 2, |
| -HANDWRITING_BOUNDS_OFFSET_PX - 50); |
| // keyboard shouldn't show up. |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", marker), |
| NOT_EXPECT_TIMEOUT); |
| // Handwriting should not start |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartStylusHandwriting", marker), |
| NOT_EXPECT_TIMEOUT); |
| |
| verifyStylusHandwritingWindowIsNotShown(stream, imeSession); |
| } |
| } |
| |
| @Test |
| /** |
| * Inject Stylus events on top of an unfocused editor and verify Handwriting is started and |
| * InkWindow is displayed. |
| */ |
| public void testHandwriting_unfocusedEditText() throws Exception { |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String focusedMarker = getTestMarker(); |
| final String unfocusedMarker = getTestMarker(); |
| final Pair<EditText, EditText> editTextPair = |
| launchTestActivity(focusedMarker, unfocusedMarker); |
| final EditText unfocusedEditText = editTextPair.second; |
| |
| expectEvent(stream, editorMatcher("onStartInput", focusedMarker), TIMEOUT); |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", focusedMarker), |
| NOT_EXPECT_TIMEOUT); |
| |
| addVirtualStylusIdForTestSession(); |
| final int touchSlop = getTouchSlop(); |
| final int startX = unfocusedEditText.getWidth() / 2; |
| final int startY = 2 * touchSlop; |
| // (endX, endY) is out of bound to avoid that unfocusedEditText is focused due to the |
| // stylus touch. |
| final int endX = startX; |
| final int endY = unfocusedEditText.getHeight() + 2 * touchSlop; |
| final int number = 5; |
| |
| TestUtils.injectStylusDownEvent(unfocusedEditText, startX, startY); |
| TestUtils.injectStylusMoveEvents(unfocusedEditText, startX, startY, |
| endX, endY, number); |
| // Handwriting should already be initiated before ACTION_UP. |
| // unfocusedEditor is focused and triggers onStartInput. |
| expectEvent(stream, editorMatcher("onStartInput", unfocusedMarker), TIMEOUT); |
| // keyboard shouldn't show up. |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", unfocusedMarker), |
| NOT_EXPECT_TIMEOUT); |
| // Handwriting should start on the unfocused EditText. |
| expectEvent( |
| stream, |
| editorMatcher("onStartStylusHandwriting", unfocusedMarker), |
| TIMEOUT); |
| |
| verifyStylusHandwritingWindowIsShown(stream, imeSession); |
| |
| TestUtils.injectStylusUpEvent(unfocusedEditText, endX, endY); |
| } |
| } |
| |
| |
| @Test |
| /** |
| * Inject Stylus events on top of an unfocused editor which disabled the autoHandwriting and |
| * verify Handwriting is not started and InkWindow is not displayed. |
| */ |
| public void testHandwriting_unfocusedEditText_autoHandwritingDisabled() throws Exception { |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String focusedMarker = getTestMarker(); |
| final String unfocusedMarker = getTestMarker(); |
| final Pair<EditText, EditText> editTextPair = |
| launchTestActivity(focusedMarker, unfocusedMarker); |
| final EditText unfocusedEditText = editTextPair.second; |
| unfocusedEditText.setAutoHandwritingEnabled(false); |
| |
| expectEvent(stream, editorMatcher("onStartInput", focusedMarker), TIMEOUT); |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", focusedMarker), |
| NOT_EXPECT_TIMEOUT); |
| |
| addVirtualStylusIdForTestSession(); |
| final int touchSlop = getTouchSlop(); |
| final int startX = unfocusedEditText.getWidth() / 2; |
| final int startY = 2 * touchSlop; |
| // (endX, endY) is out of bound to avoid that unfocusedEditText is focused due to the |
| // stylus touch. |
| final int endX = startX; |
| final int endY = -2 * touchSlop; |
| final int number = 5; |
| TestUtils.injectStylusDownEvent(unfocusedEditText, startX, startY); |
| TestUtils.injectStylusMoveEvents(unfocusedEditText, startX, startY, |
| endX, endY, number); |
| TestUtils.injectStylusUpEvent(unfocusedEditText, endX, endY); |
| |
| // unfocusedEditor opts out autoHandwriting, so it won't trigger onStartInput. |
| notExpectEvent(stream, editorMatcher("onStartInput", unfocusedMarker), TIMEOUT); |
| // keyboard shouldn't show up. |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", unfocusedMarker), |
| NOT_EXPECT_TIMEOUT); |
| // Handwriting should not start |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartStylusHandwriting", unfocusedMarker), |
| NOT_EXPECT_TIMEOUT); |
| |
| verifyStylusHandwritingWindowIsNotShown(stream, imeSession); |
| } |
| } |
| |
| /** |
| * Inject stylus top on an editor and verify stylus source is detected with |
| * {@link InputMethodService#onUpdateEditorToolType(int)} lifecycle method. |
| */ |
| @Test |
| @FlakyTest |
| public void testOnViewClicked_withStylusTap() throws Exception { |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String marker = getTestMarker(); |
| final String marker2 = getTestMarker(); |
| final Pair<EditText, EditText> pair = launchTestActivity(marker, marker2); |
| final EditText focusedEditText = pair.first; |
| final EditText unfocusedEditText = pair.second; |
| |
| expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", marker), |
| NOT_EXPECT_TIMEOUT); |
| |
| final int startX = focusedEditText.getWidth() / 2; |
| final int startY = focusedEditText.getHeight() / 2; |
| |
| // Tap with stylus on focused editor |
| TestUtils.injectStylusDownEvent(focusedEditText, startX, startY); |
| MotionEvent event = TestUtils.injectStylusUpEvent(focusedEditText, startX, startY); |
| int toolType = event.getToolType(event.getActionIndex()); |
| |
| expectEvent( |
| stream, |
| onUpdateEditorToolTypeMatcher(toolType), |
| TIMEOUT); |
| |
| // Tap with stylus on unfocused editor |
| TestUtils.injectStylusDownEvent(unfocusedEditText, startX, startY); |
| event = TestUtils.injectStylusUpEvent(unfocusedEditText, startX, startY); |
| expectEvent(stream, onStartInputMatcher(toolType, marker2), TIMEOUT); |
| expectEvent( |
| stream, |
| onUpdateEditorToolTypeMatcher(event.getToolType(event.getActionIndex())), |
| TIMEOUT); |
| } |
| } |
| |
| /** |
| * Inject finger top on an editor and verify stylus source is detected with |
| * {@link InputMethodService#onUpdateEditorToolType(int)} lifecycle method. |
| */ |
| @Test |
| @FlakyTest |
| public void testOnViewClicked_withFingerTap() throws Exception { |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String marker = getTestMarker(); |
| final String marker2 = getTestMarker(); |
| final Pair<EditText, EditText> pair = launchTestActivity(marker, marker2); |
| final EditText focusedEditText = pair.first; |
| final EditText unfocusedEditText = pair.second; |
| |
| expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", marker), |
| NOT_EXPECT_TIMEOUT); |
| |
| TestUtils.injectFingerEventOnViewCenter(focusedEditText, MotionEvent.ACTION_DOWN); |
| MotionEvent upEvent = |
| TestUtils.injectFingerEventOnViewCenter(focusedEditText, MotionEvent.ACTION_UP); |
| int toolTypeFinger = upEvent.getToolType(upEvent.getActionIndex()); |
| assertEquals( |
| "tool type finger must match", MotionEvent.TOOL_TYPE_FINGER, toolTypeFinger); |
| expectEvent( |
| stream, |
| onUpdateEditorToolTypeMatcher(toolTypeFinger), |
| TIMEOUT); |
| |
| // tap on unfocused editor |
| TestUtils.injectFingerEventOnViewCenter(unfocusedEditText, MotionEvent.ACTION_DOWN); |
| upEvent = TestUtils.injectFingerEventOnViewCenter( |
| unfocusedEditText, MotionEvent.ACTION_UP); |
| toolTypeFinger = upEvent.getToolType(upEvent.getActionIndex()); |
| assertEquals( |
| "tool type finger must match", MotionEvent.TOOL_TYPE_FINGER, toolTypeFinger); |
| expectEvent(stream, onStartInputMatcher(toolTypeFinger, marker2), TIMEOUT); |
| expectEvent( |
| stream, |
| onUpdateEditorToolTypeMatcher(MotionEvent.TOOL_TYPE_FINGER), |
| TIMEOUT); |
| } |
| } |
| |
| /** |
| * Inject stylus handwriting event on an editor and verify stylus source is detected with |
| * {@link InputMethodService#onUpdateEditorToolType(int)} on next startInput(). |
| */ |
| @Test |
| @FlakyTest |
| public void testOnViewClicked_withStylusHandwriting() throws Exception { |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| addVirtualStylusIdForTestSession(); |
| |
| final String focusedMarker = getTestMarker(); |
| final String unFocusedMarker = getTestMarker(); |
| final Pair<EditText, EditText> pair = |
| launchTestActivity(focusedMarker, unFocusedMarker); |
| final EditText focusedEditText = pair.first; |
| final EditText unfocusedEditText = pair.second; |
| |
| expectEvent(stream, editorMatcher("onStartInput", focusedMarker), TIMEOUT); |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", focusedMarker), |
| NOT_EXPECT_TIMEOUT); |
| |
| // Finger tap on editor and verify onUpdateEditorToolType |
| TestUtils.injectFingerEventOnViewCenter(focusedEditText, MotionEvent.ACTION_DOWN); |
| MotionEvent upEvent = |
| TestUtils.injectFingerEventOnViewCenter(focusedEditText, MotionEvent.ACTION_UP); |
| int toolTypeFinger = upEvent.getToolType(upEvent.getActionIndex()); |
| assertEquals( |
| "tool type finger must match", MotionEvent.TOOL_TYPE_FINGER, toolTypeFinger); |
| expectEvent( |
| stream, |
| onUpdateEditorToolTypeMatcher(toolTypeFinger), |
| TIMEOUT); |
| |
| // Start handwriting on same focused editor |
| final int touchSlop = getTouchSlop(); |
| int startX = focusedEditText.getWidth() / 2; |
| int startY = focusedEditText.getHeight() / 2; |
| int endX = startX + 2 * touchSlop; |
| int endY = startY + 2 * touchSlop; |
| final int number = 5; |
| TestUtils.injectStylusDownEvent(focusedEditText, startX, startY); |
| TestUtils.injectStylusMoveEvents(focusedEditText, startX, startY, |
| endX, endY, number); |
| // Handwriting should start. |
| expectEvent( |
| stream, |
| editorMatcher("onStartStylusHandwriting", focusedMarker), |
| TIMEOUT); |
| TestUtils.injectStylusUpEvent(focusedEditText, endX, endY); |
| imeSession.callFinishStylusHandwriting(); |
| expectEvent( |
| stream, |
| editorMatcher("onFinishStylusHandwriting", focusedMarker), |
| TIMEOUT_1_S); |
| |
| addVirtualStylusIdForTestSession(); |
| // Now start handwriting on unfocused editor and verify toolType is available in |
| // EditorInfo |
| startX = unfocusedEditText.getWidth() / 2; |
| startY = unfocusedEditText.getHeight() / 2; |
| endX = startX + 2 * touchSlop; |
| endY = startY + 2 * touchSlop; |
| |
| TestUtils.injectStylusDownEvent(unfocusedEditText, startX, startY); |
| TestUtils.injectStylusMoveEvents(unfocusedEditText, startX, startY, |
| endX, endY, number); |
| expectEvent(stream, editorMatcher("onStartInput", unFocusedMarker), TIMEOUT); |
| |
| // toolType should be updated on next stylus handwriting start |
| expectEvent(stream, onStartStylusHandwritingMatcher( |
| MotionEvent.TOOL_TYPE_STYLUS, unFocusedMarker), TIMEOUT); |
| |
| TestUtils.injectStylusUpEvent(unfocusedEditText, endX, endY); |
| } |
| } |
| |
| private static Predicate<ImeEvent> onStartInputMatcher(int toolType, String marker) { |
| Predicate<ImeEvent> matcher = event -> { |
| if (!TextUtils.equals("onStartInput", event.getEventName())) { |
| return false; |
| } |
| EditorInfo info = event.getArguments().getParcelable("editorInfo"); |
| return info.getInitialToolType() == toolType |
| && TextUtils.equals(marker, info.privateImeOptions); |
| }; |
| return withDescription( |
| "onStartInput(initialToolType=" + toolType + ",marker=" + marker + ")", matcher); |
| } |
| |
| private static Predicate<ImeEvent> onStartStylusHandwritingMatcher( |
| int toolType, String marker) { |
| Predicate<ImeEvent> matcher = event -> { |
| if (!TextUtils.equals("onStartStylusHandwriting", event.getEventName())) { |
| return false; |
| } |
| EditorInfo info = event.getArguments().getParcelable("editorInfo"); |
| return info.getInitialToolType() == toolType |
| && TextUtils.equals(marker, info.privateImeOptions); |
| }; |
| return withDescription( |
| "onStartStylusHandwriting(initialToolType=" + toolType |
| + ", marker=" + marker + ")", matcher); |
| } |
| |
| private static Predicate<ImeEvent> onUpdateEditorToolTypeMatcher(int expectedToolType) { |
| Predicate<ImeEvent> matcher = event -> { |
| if (!TextUtils.equals("onUpdateEditorToolType", event.getEventName())) { |
| return false; |
| } |
| final int actualToolType = event.getArguments().getInt("toolType"); |
| return actualToolType == expectedToolType; |
| }; |
| return withDescription("onUpdateEditorToolType(toolType=" + expectedToolType + ")", |
| matcher); |
| } |
| |
| /** |
| * Inject stylus events on top of a focused custom editor and verify handwriting is started and |
| * stylus handwriting window is displayed. |
| */ |
| @Test |
| public void testHandwriting_focusedCustomEditor() throws Exception { |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String focusedMarker = getTestMarker(); |
| final String unfocusedMarker = getTestMarker(); |
| final Pair<CustomEditorView, CustomEditorView> customEditorPair = |
| launchTestActivityWithCustomEditors(focusedMarker, unfocusedMarker); |
| final CustomEditorView focusedCustomEditor = customEditorPair.first; |
| |
| expectEvent(stream, editorMatcher("onStartInput", focusedMarker), TIMEOUT); |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", focusedMarker), |
| NOT_EXPECT_TIMEOUT); |
| |
| addVirtualStylusIdForTestSession(); |
| |
| injectStylusEventToEditorAndVerify(focusedCustomEditor, stream, imeSession, |
| focusedMarker, true /* verifyHandwritingStart */, |
| true /* verifyHandwritingWindowShown */, |
| false /* verifyHandwritingWindowNotShown */); |
| |
| // Verify that stylus move events are swallowed by the handwriting initiator once |
| // handwriting has been initiated and not dispatched to the view tree. |
| assertThat(focusedCustomEditor.mStylusMoveEventCount) |
| .isLessThan(NUMBER_OF_INJECTED_EVENTS); |
| } |
| } |
| |
| /** |
| * Inject stylus events on top of a handwriting initiation delegate view and verify handwriting |
| * is started on the delegator editor and stylus handwriting window is displayed. |
| */ |
| @Test |
| public void testHandwriting_delegate() throws Exception { |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String editTextMarker = getTestMarker(); |
| final View delegateView = |
| launchTestActivityWithDelegate( |
| editTextMarker, null /* delegateLatch */, 0 /* delegateDelayMs */); |
| expectBindInput(stream, Process.myPid(), TIMEOUT); |
| addVirtualStylusIdForTestSession(); |
| |
| // After injecting DOWN and MOVE events, the handwriting initiator should trigger the |
| // delegate view's callback which creates the EditText and requests focus, which should |
| // then initiate handwriting for the EditText. |
| injectStylusEventToEditorAndVerify(delegateView, stream, imeSession, |
| editTextMarker, true /* verifyHandwritingStart */, |
| true /* verifyHandwritingWindowShown */, |
| false /* verifyHandwritingWindowNotShown */); |
| } |
| } |
| |
| /** |
| * Inject stylus events on top of a handwriting initiation delegator view and verify handwriting |
| * is started on the delegate editor, even though delegate took a little time to |
| * acceptStylusHandwriting(). |
| */ |
| @Test |
| public void testHandwriting_delegateDelayed() throws Exception { |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String editTextMarker = getTestMarker(); |
| final CountDownLatch latch = new CountDownLatch(1); |
| // Use a delegate that executes after 1 second delay. |
| final View delegatorView = |
| launchTestActivityWithDelegate(editTextMarker, latch, TIMEOUT_1_S); |
| expectBindInput(stream, Process.myPid(), TIMEOUT); |
| addVirtualStylusIdForTestSession(); |
| |
| final int touchSlop = getTouchSlop(); |
| final int startX = delegatorView.getWidth() / 2; |
| final int startY = delegatorView.getHeight() / 2; |
| final int endX = startX + 2 * touchSlop; |
| final int endY = startY + 2 * touchSlop; |
| final int number = 5; |
| TestUtils.injectStylusDownEvent(delegatorView, startX, startY); |
| TestUtils.injectStylusMoveEvents(delegatorView, startX, startY, endX, endY, number); |
| // Wait until delegate makes request. |
| latch.await(DELEGATION_AFTER_IDLE_TIMEOUT_MS, TimeUnit.MILLISECONDS); |
| // Keyboard shouldn't show up. |
| notExpectEvent( |
| stream, editorMatcher("onStartInputView", editTextMarker), NOT_EXPECT_TIMEOUT); |
| // Handwriting should start since delegation was delayed (but still before timeout). |
| expectEvent( |
| stream, editorMatcher("onStartStylusHandwriting", editTextMarker), TIMEOUT); |
| verifyStylusHandwritingWindowIsShown(stream, imeSession); |
| |
| TestUtils.injectStylusUpEvent(delegatorView, endX, endY); |
| } |
| } |
| |
| /** |
| * Inject stylus events on top of a handwriting initiation delegator view and verify handwriting |
| * is not started on the delegate editor after delegate idle-timeout. |
| */ |
| @Test |
| public void testHandwriting_delegateAfterTimeout() throws Exception { |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String editTextMarker = getTestMarker(); |
| final CountDownLatch latch = new CountDownLatch(1); |
| // Use a delegate that executes after idle-timeout. |
| final View delegatorView = |
| launchTestActivityWithDelegate( |
| editTextMarker, latch, DELEGATION_AFTER_IDLE_TIMEOUT_MS); |
| expectBindInput(stream, Process.myPid(), TIMEOUT); |
| addVirtualStylusIdForTestSession(); |
| |
| final int touchSlop = getTouchSlop(); |
| final int startX = delegatorView.getWidth() / 2; |
| final int startY = delegatorView.getHeight() / 2; |
| final int endX = startX + 2 * touchSlop; |
| final int endY = startY + 2 * touchSlop; |
| final int number = 5; |
| TestUtils.injectStylusDownEvent(delegatorView, startX, startY); |
| TestUtils.injectStylusMoveEvents(delegatorView, startX, startY, endX, endY, number); |
| // Wait until delegate makes request. |
| latch.await(DELEGATION_AFTER_IDLE_TIMEOUT_MS, TimeUnit.MILLISECONDS); |
| // Keyboard shouldn't show up. |
| notExpectEvent( |
| stream, editorMatcher("onStartInputView", editTextMarker), NOT_EXPECT_TIMEOUT); |
| // Handwriting should *not* start since delegation was idle timed-out. |
| notExpectEvent( |
| stream, editorMatcher("onStartStylusHandwriting", editTextMarker), TIMEOUT); |
| verifyStylusHandwritingWindowIsNotShown(stream, imeSession); |
| |
| TestUtils.injectStylusUpEvent(delegatorView, endX, endY); |
| } |
| } |
| |
| /** |
| * Inject stylus events on top of a handwriting initiation delegate view and verify handwriting |
| * is started on the delegator editor [in different package] and stylus handwriting is |
| * started. |
| * TODO(b/210039666): support instant apps for this test. |
| */ |
| @AppModeFull(reason = "Launching external activity from this test is not yet supported.") |
| @Test |
| public void testHandwriting_delegateToDifferentPackage() throws Exception { |
| testHandwriting_delegateToDifferentPackage(true /* setAllowedDelegatorPackage */); |
| } |
| |
| /** |
| * Inject stylus events on top of a handwriting initiation delegate view and verify handwriting |
| * is not started on the delegator editor [in different package] because allowed package wasn't |
| * set. |
| * TODO(b/210039666): support instant apps for this test. |
| */ |
| @AppModeFull(reason = "Launching external activity from this test is not yet supported.") |
| @Test |
| public void testHandwriting_delegateToDifferentPackage_fail() throws Exception { |
| testHandwriting_delegateToDifferentPackage(false /* setAllowedDelegatorPackage */); |
| } |
| |
| private void testHandwriting_delegateToDifferentPackage(boolean setAllowedDelegatorPackage) |
| throws Exception { |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String editTextMarker = getTestMarker(); |
| final View delegateView = |
| launchTestActivityInExternalPackage(editTextMarker, setAllowedDelegatorPackage); |
| expectBindInput(stream, Process.myPid(), TIMEOUT); |
| addVirtualStylusIdForTestSession(); |
| |
| final int touchSlop = getTouchSlop(); |
| final int startX = delegateView.getWidth() / 2; |
| final int startY = delegateView.getHeight() / 2; |
| final int endX = startX + 2 * touchSlop; |
| final int endY = startY + 2 * touchSlop; |
| final int number = 5; |
| TestUtils.injectStylusDownEvent(delegateView, startX, startY); |
| TestUtils.injectStylusMoveEvents(delegateView, startX, startY, endX, endY, number); |
| |
| // Keyboard shouldn't show up. |
| notExpectEvent( |
| stream, editorMatcher("onStartInputView", editTextMarker), |
| NOT_EXPECT_TIMEOUT); |
| if (setAllowedDelegatorPackage) { |
| expectEvent( |
| stream, editorMatcher("onStartStylusHandwriting", editTextMarker), TIMEOUT); |
| verifyStylusHandwritingWindowIsShown(stream, imeSession); |
| } else { |
| notExpectEvent( |
| stream, editorMatcher("onStartStylusHandwriting", editTextMarker), |
| NOT_EXPECT_TIMEOUT); |
| } |
| } |
| } |
| |
| /** |
| * Inject stylus events on top of a handwriting initiation delegator view in the default |
| * launcher activity, and verify stylus handwriting is started on the delegate editor (in a |
| * different package]. |
| * TODO(b/210039666): Support instant apps for this test. |
| */ |
| @Test |
| @RequiresFlagsEnabled(FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR) |
| @AppModeFull(reason = "Launching external activity from this test is not yet supported.") |
| public void testHandwriting_delegateFromHomePackage() throws Exception { |
| testHandwriting_delegateFromHomePackage(/* setHomeDelegatorAllowed= */ true); |
| } |
| |
| /** |
| * Inject stylus events on top of a handwriting initiation delegator view in the default |
| * launcher activity, and verify stylus handwriting is not started on the delegate editor (in a |
| * different package] because {@link View#setHomeScreenHandwritingDelegatorAllowed} wasn't set. |
| * TODO(b/210039666): Support instant apps for this test. |
| */ |
| @Test |
| @RequiresFlagsEnabled(FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR) |
| @AppModeFull(reason = "Launching external activity from this test is not yet supported.") |
| public void testHandwriting_delegateFromHomePackage_fail() throws Exception { |
| testHandwriting_delegateFromHomePackage(/* setHomeDelegatorAllowed= */ false); |
| } |
| |
| public void testHandwriting_delegateFromHomePackage(boolean setHomeDelegatorAllowed) |
| throws Exception { |
| mDefaultLauncherToRestore = getDefaultLauncher(); |
| setDefaultLauncher(TEST_LAUNCHER_COMPONENT); |
| |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| ImeEventStream stream = imeSession.openEventStream(); |
| |
| String editTextMarker = getTestMarker(); |
| |
| // Start launcher activity |
| Intent intent = new Intent(Intent.ACTION_MAIN); |
| intent.addCategory(Intent.CATEGORY_HOME); |
| intent.addCategory(Intent.CATEGORY_DEFAULT); |
| intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| // LauncherActivity passes these three extras to the ctstestapp MainActivity |
| intent.putExtra(MockTestActivityUtil.EXTRA_KEY_PRIVATE_IME_OPTIONS, editTextMarker); |
| intent.putExtra(MockTestActivityUtil.EXTRA_HANDWRITING_DELEGATE, true); |
| intent.putExtra( |
| MockTestActivityUtil.EXTRA_HOME_HANDWRITING_DELEGATOR_ALLOWED, |
| setHomeDelegatorAllowed); |
| InstrumentationRegistry.getInstrumentation().getContext().startActivity(intent); |
| |
| expectBindInput(stream, Process.myPid(), TIMEOUT); |
| addVirtualStylusIdForTestSession(); |
| |
| // Launcher activity displays a full screen handwriting delegator view. Stylus events |
| // are injected in the center of the screen to trigger the delegator callback, which |
| // launches the ctstestapp MainActivity with the delegate editor with editTextMarker. |
| DisplayMetrics metrics = mContext.getResources().getDisplayMetrics(); |
| int touchSlop = getTouchSlop(); |
| int startX = metrics.widthPixels / 2; |
| int startY = metrics.heightPixels / 2; |
| int endX = startX + 2 * touchSlop; |
| int endY = startY + 2 * touchSlop; |
| View mockView = mock(View.class); |
| TestUtils.injectStylusDownEvent(mockView, startX, startY); |
| TestUtils.injectStylusMoveEvents(mockView, startX, startY, endX, endY, 5); |
| |
| // Keyboard shouldn't show up. |
| notExpectEvent( |
| stream, editorMatcher("onStartInputView", editTextMarker), NOT_EXPECT_TIMEOUT); |
| if (setHomeDelegatorAllowed) { |
| expectEvent( |
| stream, editorMatcher("onStartStylusHandwriting", editTextMarker), TIMEOUT); |
| verifyStylusHandwritingWindowIsShown(stream, imeSession); |
| } else { |
| notExpectEvent( |
| stream, editorMatcher("onStartStylusHandwriting", editTextMarker), |
| NOT_EXPECT_TIMEOUT); |
| } |
| } |
| } |
| |
| /** |
| * Verify that system times-out Handwriting session after given timeout. |
| */ |
| @Test |
| public void testHandwritingSessionIdleTimeout() throws Exception { |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String marker = getTestMarker(); |
| final EditText editText = launchTestActivity(marker); |
| |
| expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", marker), |
| NOT_EXPECT_TIMEOUT); |
| |
| addVirtualStylusIdForTestSession(); |
| // update handwriting session timeout |
| assertTrue(expectCommand( |
| stream, |
| imeSession.callSetStylusHandwritingTimeout(100 /* timeoutMs */), |
| TIMEOUT).getReturnBooleanValue()); |
| |
| injectStylusEventToEditorAndVerify(editText, stream, imeSession, |
| marker, true /* verifyHandwritingStart */, |
| false /* verifyHandwritingWindowShown */, |
| false /* verifyHandwritingWindowNotShown */); |
| |
| // Handwriting should finish soon. |
| expectEvent( |
| stream, |
| editorMatcher("onFinishStylusHandwriting", marker), |
| TIMEOUT_1_S); |
| |
| // test setting extremely large timeout and verify we limit it to |
| // STYLUS_HANDWRITING_IDLE_TIMEOUT_MS |
| assertTrue(expectCommand( |
| stream, imeSession.callSetStylusHandwritingTimeout( |
| InputMethodService.getStylusHandwritingIdleTimeoutMax().toMillis() |
| * 10), |
| TIMEOUT).getReturnBooleanValue()); |
| assertEquals("Stylus handwriting timeout must be equal to max value.", |
| InputMethodService.getStylusHandwritingIdleTimeoutMax().toMillis(), |
| expectCommand( |
| stream, imeSession.callGetStylusHandwritingTimeout(), TIMEOUT) |
| .getReturnLongValue()); |
| } |
| } |
| |
| @Test |
| public void testHandwritingFinishesOnUnbind() throws Exception { |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String marker = getTestMarker(); |
| final EditText editText = launchTestActivity(marker); |
| |
| expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", marker), |
| NOT_EXPECT_TIMEOUT); |
| |
| addVirtualStylusIdForTestSession(); |
| |
| final int touchSlop = getTouchSlop(); |
| final int startX = editText.getWidth() / 2; |
| final int startY = editText.getHeight() / 2; |
| final int endX = startX + 2 * touchSlop; |
| final int endY = startY; |
| final int number = 5; |
| TestUtils.injectStylusDownEvent(editText, startX, startY); |
| TestUtils.injectStylusMoveEvents(editText, startX, startY, |
| endX, endY, number); |
| |
| expectEvent( |
| stream, |
| editorMatcher("onStartStylusHandwriting", marker), |
| TIMEOUT); |
| // Unbind IME and verify finish is called |
| ((Activity) editText.getContext()).finish(); |
| |
| // Handwriting should finish soon. |
| expectEvent( |
| stream, |
| editorMatcher("onFinishStylusHandwriting", marker), |
| TIMEOUT_1_S); |
| verifyStylusHandwritingWindowIsNotShown(stream, imeSession); |
| } |
| } |
| |
| /** |
| * Verify that system remove handwriting window immediately when timeout is small |
| */ |
| @Test |
| public void testHandwritingWindowRemoval_immediate() throws Exception { |
| final InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String marker = getTestMarker(); |
| final EditText editText = launchTestActivity(marker); |
| |
| expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", marker), |
| NOT_EXPECT_TIMEOUT); |
| |
| addVirtualStylusIdForTestSession(); |
| // update handwriting window timeout to a small value so that it is removed immediately. |
| SystemUtil.runWithShellPermissionIdentity(() -> |
| imm.setStylusWindowIdleTimeoutForTest(100)); |
| |
| final int touchSlop = getTouchSlop(); |
| final int startX = editText.getWidth() / 2; |
| final int startY = editText.getHeight() / 2; |
| final int endX = startX + 2 * touchSlop; |
| final int endY = startY; |
| final int number = 5; |
| TestUtils.injectStylusDownEvent(editText, startX, startY); |
| TestUtils.injectStylusMoveEvents(editText, startX, startY, |
| endX, endY, number); |
| // Handwriting should already be initiated before ACTION_UP. |
| // keyboard shouldn't show up. |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", marker), |
| NOT_EXPECT_TIMEOUT); |
| // Handwriting should start |
| expectEvent( |
| stream, |
| editorMatcher("onStartStylusHandwriting", marker), |
| TIMEOUT); |
| TestUtils.injectStylusUpEvent(editText, startX, startY); |
| |
| // Handwriting should finish soon. |
| expectEvent( |
| stream, |
| editorMatcher("onFinishStylusHandwriting", marker), |
| TIMEOUT_1_S); |
| verifyStylusHandwritingWindowIsNotShown(stream, imeSession); |
| // Verify handwriting window is removed. |
| assertFalse(expectCommand( |
| stream, imeSession.callHasStylusHandwritingWindow(), TIMEOUT_1_S) |
| .getReturnBooleanValue()); |
| } |
| } |
| |
| |
| /** |
| * Verify that system remove handwriting window after timeout |
| */ |
| @Test |
| public void testHandwritingWindowRemoval_afterDelay() throws Exception { |
| final InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| // skip this test if device doesn't have stylus. |
| // stylus is required, otherwise stylus virtual deviceId is removed on finishInput and |
| // we cannot test InkWindow living beyond finishHandwriting. |
| assumeTrue("Skipping test on devices that don't have stylus connected.", |
| hasSupportedStylus()); |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String marker = getTestMarker(); |
| final EditText editText = launchTestActivity(marker); |
| |
| expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", marker), |
| NOT_EXPECT_TIMEOUT); |
| |
| final int touchSlop = getTouchSlop(); |
| final int startX = editText.getWidth() / 2; |
| final int startY = editText.getHeight() / 2; |
| final int endX = startX + 2 * touchSlop; |
| final int endY = startY; |
| final int number = 5; |
| |
| // Set a larger timeout and verify handwriting window exists after unbind. |
| SystemUtil.runWithShellPermissionIdentity(() -> |
| imm.setStylusWindowIdleTimeoutForTest(TIMEOUT)); |
| |
| TestUtils.injectStylusDownEvent(editText, startX, startY); |
| TestUtils.injectStylusMoveEvents(editText, startX, startY, |
| endX, endY, number); |
| // Handwriting should already be initiated before ACTION_UP. |
| // Handwriting should start |
| expectEvent( |
| stream, |
| editorMatcher("onStartStylusHandwriting", marker), |
| TIMEOUT); |
| TestUtils.injectStylusUpEvent(editText, startX, startY); |
| |
| // Handwriting should finish soon. |
| notExpectEvent( |
| stream, |
| editorMatcher("onFinishStylusHandwriting", marker), |
| TIMEOUT_1_S); |
| verifyStylusHandwritingWindowIsShown(stream, imeSession); |
| // Verify handwriting window exists. |
| assertTrue(expectCommand( |
| stream, imeSession.callHasStylusHandwritingWindow(), TIMEOUT_1_S) |
| .getReturnBooleanValue()); |
| |
| // Finish activity and IME window should be invisible. |
| ((Activity) editText.getContext()).finish(); |
| verifyStylusHandwritingWindowIsNotShown(stream, imeSession); |
| // Verify handwriting window isn't removed immediately. |
| assertTrue(expectCommand( |
| stream, imeSession.callHasStylusHandwritingWindow(), TIMEOUT_1_S) |
| .getReturnBooleanValue()); |
| // Verify handwriting window is eventually removed (within timeout). |
| CommonTestUtils.waitUntil("Stylus handwriting window should be removed", |
| TIMEOUT_IN_SECONDS, |
| () -> !expectCommand( |
| stream, imeSession.callHasStylusHandwritingWindow(), TIMEOUT) |
| .getReturnBooleanValue()); |
| } |
| } |
| |
| /** |
| * Verify that when system has no stylus, there is no handwriting window. |
| */ |
| @Test |
| @ApiTest(apis = {"android.view.inputmethod.InputMethodManager#startStylusHandwriting", |
| "android.inputmethodservice.InputMethodService#onStartStylusHandwriting", |
| "android.inputmethodservice.InputMethodService#onFinishStylusHandwriting"}) |
| public void testNoStylusNoHandwritingWindow() throws Exception { |
| // skip this test if device already has stylus. |
| assumeFalse("Skipping test on devices that have stylus connected.", |
| hasSupportedStylus()); |
| |
| final InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String marker = getTestMarker(); |
| final EditText editText = launchTestActivity(marker); |
| |
| expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", marker), |
| NOT_EXPECT_TIMEOUT); |
| |
| // Verify there is no handwriting window before stylus is added. |
| assertFalse(expectCommand( |
| stream, imeSession.callHasStylusHandwritingWindow(), TIMEOUT_1_S) |
| .getReturnBooleanValue()); |
| |
| addVirtualStylusIdForTestSession(); |
| |
| injectStylusEventToEditorAndVerify(editText, stream, imeSession, |
| marker, true /* verifyHandwritingStart */, |
| true /* verifyHandwritingWindowShown */, |
| false /* verifyHandwritingWindowNotShown */); |
| |
| // Finish handwriting to remove test stylus id. |
| imeSession.callFinishStylusHandwriting(); |
| expectEvent( |
| stream, |
| editorMatcher("onFinishStylusHandwriting", marker), |
| TIMEOUT_1_S); |
| |
| // Verify no handwriting window after stylus is removed from device. |
| assertFalse(expectCommand( |
| stream, imeSession.callHasStylusHandwritingWindow(), TIMEOUT_1_S) |
| .getReturnBooleanValue()); |
| |
| } |
| } |
| |
| /** |
| * Verifies that in split-screen multi-window mode, unfocused activity can start handwriting |
| */ |
| @Test |
| @ApiTest(apis = {"android.view.inputmethod.InputMethodManager#startStylusHandwriting", |
| "android.inputmethodservice.InputMethodService#onStartStylusHandwriting", |
| "android.inputmethodservice.InputMethodService#onFinishStylusHandwriting"}) |
| public void testMultiWindow_unfocusedWindowCanStartHandwriting() throws Exception { |
| assumeTrue(TestUtils.supportsSplitScreenMultiWindow()); |
| |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| final String primaryMarker = getTestMarker(); |
| final String secondaryMarker = getTestMarker(); |
| |
| // Launch an editor activity to be on the split primary task. |
| final TestActivity splitPrimaryActivity = TestActivity.startSync(activity -> { |
| final LinearLayout layout = new LinearLayout(activity); |
| layout.setOrientation(LinearLayout.VERTICAL); |
| final EditText editText = new EditText(activity); |
| layout.addView(editText); |
| editText.setHint("focused editText"); |
| editText.setPrivateImeOptions(primaryMarker); |
| editText.requestFocus(); |
| return layout; |
| }); |
| expectEvent(stream, editorMatcher("onStartInput", primaryMarker), TIMEOUT); |
| notExpectEvent(stream, editorMatcher("onStartInputView", primaryMarker), |
| NOT_EXPECT_TIMEOUT); |
| |
| TestUtils.waitOnMainUntil(() -> splitPrimaryActivity.hasWindowFocus(), TIMEOUT); |
| |
| // Launch another activity to be on the split secondary task, expect stylus gesture on |
| // it can steal focus from primary and start handwriting. |
| final AtomicReference<EditText> editTextRef = new AtomicReference<>(); |
| final TestActivity splitSecondaryActivity = new TestActivity.Starter() |
| .asMultipleTask() |
| .withAdditionalFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT) |
| .startSync(splitPrimaryActivity, activity -> { |
| final LinearLayout layout = new LinearLayout(activity); |
| layout.setOrientation(LinearLayout.VERTICAL); |
| final EditText editText = new EditText(activity); |
| editTextRef.set(editText); |
| layout.addView(editText); |
| editText.setHint("unfocused editText"); |
| editText.setPrivateImeOptions(secondaryMarker); |
| return layout; |
| }, TestActivity2.class); |
| notExpectEvent(stream, event -> "onStartInputView".equals(event.getEventName()), |
| NOT_EXPECT_TIMEOUT); |
| TestUtils.waitOnMainUntil(() -> splitSecondaryActivity.hasWindowFocus(), TIMEOUT); |
| TestUtils.waitOnMainUntil(() -> !splitPrimaryActivity.hasWindowFocus(), TIMEOUT_1_S); |
| |
| addVirtualStylusIdForTestSession(); |
| |
| final EditText editText = editTextRef.get(); |
| |
| injectStylusEventToEditorAndVerify(editText, stream, imeSession, |
| secondaryMarker, true /* verifyHandwritingStart */, |
| true /* verifyHandwritingWindowShown */, |
| false /* verifyHandwritingWindowNotShown */); |
| |
| // Finish handwriting to remove test stylus id. |
| imeSession.callFinishStylusHandwriting(); |
| expectEvent( |
| stream, |
| editorMatcher("onFinishStylusHandwriting", secondaryMarker), |
| TIMEOUT_1_S); |
| } |
| } |
| |
| /** |
| * Verifies that in split-screen multi-window mode, an unfocused window can't steal ongoing |
| * handwriting session. |
| */ |
| @Test |
| @ApiTest(apis = {"android.view.inputmethod.InputMethodManager#startStylusHandwriting", |
| "android.inputmethodservice.InputMethodService#onStartStylusHandwriting", |
| "android.inputmethodservice.InputMethodService#onFinishStylusHandwriting"}) |
| public void testMultiWindow_unfocusedWindowCannotStealOngoingHandwriting() throws Exception { |
| assumeTrue(TestUtils.supportsSplitScreenMultiWindow()); |
| |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| final String primaryMarker = getTestMarker(); |
| final String secondaryMarker = getTestMarker(); |
| |
| // Launch an editor activity to be on the split primary task. |
| final AtomicReference<EditText> editTextPrimaryRef = new AtomicReference<>(); |
| final TestActivity splitPrimaryActivity = TestActivity.startSync(activity -> { |
| final LinearLayout layout = new LinearLayout(activity); |
| layout.setOrientation(LinearLayout.VERTICAL); |
| final EditText editText = new EditText(activity); |
| layout.addView(editText); |
| editTextPrimaryRef.set(editText); |
| editText.setHint("focused editText"); |
| editText.setPrivateImeOptions(primaryMarker); |
| return layout; |
| }); |
| notExpectEvent(stream, |
| editorMatcher("onStartInput", primaryMarker), NOT_EXPECT_TIMEOUT); |
| notExpectEvent(stream, |
| editorMatcher("onStartInputView", primaryMarker), NOT_EXPECT_TIMEOUT); |
| |
| TestUtils.waitOnMainUntil(() -> splitPrimaryActivity.hasWindowFocus(), TIMEOUT); |
| |
| // Launch another activity to be on the split secondary task, expect stylus gesture on |
| // it can steal focus from primary and start handwriting. |
| final AtomicReference<EditText> editTextSecondaryRef = new AtomicReference<>(); |
| new TestActivity.Starter() |
| .asMultipleTask() |
| .withAdditionalFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT) |
| .startSync(splitPrimaryActivity, activity -> { |
| final LinearLayout layout = new LinearLayout(activity); |
| layout.setOrientation(LinearLayout.VERTICAL); |
| final EditText editText = new EditText(activity); |
| editTextSecondaryRef.set(editText); |
| layout.addView(editText); |
| editText.setHint("unfocused editText"); |
| editText.setPrivateImeOptions(secondaryMarker); |
| return layout; |
| }, TestActivity2.class); |
| notExpectEvent(stream, event -> "onStartInputView".equals(event.getEventName()), |
| NOT_EXPECT_TIMEOUT); |
| |
| addVirtualStylusIdForTestSession(); |
| |
| // Inject events on primary to start handwriting. |
| final EditText editTextPrimary = editTextPrimaryRef.get(); |
| |
| injectStylusEventToEditorAndVerify(editTextPrimary, stream, imeSession, |
| primaryMarker, true /* verifyHandwritingStart */, |
| false /* verifyHandwritingWindowShown */, |
| false /* verifyHandwritingWindowNotShown */); |
| |
| TestUtils.waitOnMainUntil(() -> splitPrimaryActivity.hasWindowFocus(), TIMEOUT_1_S); |
| |
| // Inject events on secondary shouldn't start handwriting on secondary |
| // (since primary is already ongoing). |
| final EditText editTextSecondary = editTextSecondaryRef.get(); |
| |
| injectStylusEventToEditorAndVerify(editTextSecondary, stream, imeSession, |
| secondaryMarker, false /* verifyHandwritingStart */, |
| false /* verifyHandwritingWindowShown */, |
| false /* verifyHandwritingWindowNotShown */); |
| |
| TestUtils.waitOnMainUntil(() -> splitPrimaryActivity.hasWindowFocus(), TIMEOUT_1_S); |
| |
| // Finish handwriting to remove test stylus id. |
| imeSession.callFinishStylusHandwriting(); |
| expectEvent( |
| stream, |
| editorMatcher("onFinishStylusHandwriting", primaryMarker), |
| TIMEOUT_1_S); |
| } |
| } |
| |
| /** |
| * Verify that once stylus hasn't been used for more than idle-timeout, there is no handwriting |
| * window. |
| */ |
| @Test |
| @ApiTest(apis = {"android.view.inputmethod.InputMethodManager#startStylusHandwriting", |
| "android.inputmethodservice.InputMethodService#onStartStylusHandwriting", |
| "android.inputmethodservice.InputMethodService#onFinishStylusHandwriting"}) |
| public void testNoHandwritingWindow_afterIdleTimeout() throws Exception { |
| // skip this test if device doesn't have stylus. |
| assumeTrue("Skipping test on devices that don't stylus connected.", |
| hasSupportedStylus()); |
| |
| final InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String marker = getTestMarker(); |
| final EditText editText = launchTestActivity(marker); |
| |
| expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", marker), |
| NOT_EXPECT_TIMEOUT); |
| |
| SystemUtil.runWithShellPermissionIdentity(() -> |
| imm.setStylusWindowIdleTimeoutForTest(TIMEOUT)); |
| |
| injectStylusEventToEditorAndVerify(editText, stream, imeSession, |
| marker, true /* verifyHandwritingStart */, |
| true /* verifyHandwritingWindowShown */, |
| false /* verifyHandwritingWindowNotShown */); |
| |
| // Finish handwriting to remove test stylus id. |
| imeSession.callFinishStylusHandwriting(); |
| expectEvent( |
| stream, |
| editorMatcher("onFinishStylusHandwriting", marker), |
| TIMEOUT_1_S); |
| |
| // Verify handwriting window is removed after stylus handwriting idle-timeout. |
| TestUtils.waitOnMainUntil(() -> { |
| try { |
| // wait until callHasStylusHandwritingWindow returns false |
| return !expectCommand(stream, imeSession.callHasStylusHandwritingWindow(), |
| TIMEOUT).getReturnBooleanValue(); |
| } catch (TimeoutException e) { |
| e.printStackTrace(); |
| } |
| // handwriting window is still around. |
| return true; |
| }, TIMEOUT); |
| |
| // reset idle-timeout |
| SystemUtil.runWithShellPermissionIdentity(() -> |
| imm.setStylusWindowIdleTimeoutForTest(0)); |
| } |
| } |
| |
| /** |
| * Verify that Ink window is around before timeout |
| */ |
| @Test |
| @ApiTest(apis = {"android.view.inputmethod.InputMethodManager#startStylusHandwriting", |
| "android.inputmethodservice.InputMethodService#onStartStylusHandwriting", |
| "android.inputmethodservice.InputMethodService#onFinishStylusHandwriting"}) |
| public void testHandwritingWindow_beforeTimeout() throws Exception { |
| // skip this test if device doesn't have stylus. |
| assumeTrue("Skipping test on devices that don't stylus connected.", |
| hasSupportedStylus()); |
| |
| final InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String marker = getTestMarker(); |
| final EditText editText = launchTestActivity(marker); |
| |
| expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", marker), |
| NOT_EXPECT_TIMEOUT); |
| |
| SystemUtil.runWithShellPermissionIdentity(() -> |
| imm.setStylusWindowIdleTimeoutForTest(TIMEOUT)); |
| |
| injectStylusEventToEditorAndVerify(editText, stream, imeSession, |
| marker, true /* verifyHandwritingStart */, |
| true /* verifyHandwritingWindowShown */, |
| false /* verifyHandwritingWindowNotShown */); |
| |
| // Finish handwriting to remove test stylus id. |
| imeSession.callFinishStylusHandwriting(); |
| expectEvent( |
| stream, |
| editorMatcher("onFinishStylusHandwriting", marker), |
| TIMEOUT_1_S); |
| |
| // Just any stylus events to delay idle-timeout |
| TestUtils.injectStylusDownEvent(editText, 0, 0); |
| TestUtils.injectStylusUpEvent(editText, 0, 0); |
| |
| // Verify handwriting window is still around as stylus was used recently. |
| assertTrue(expectCommand( |
| stream, imeSession.callHasStylusHandwritingWindow(), TIMEOUT_1_S) |
| .getReturnBooleanValue()); |
| |
| // Reset idle-timeout |
| SystemUtil.runWithShellPermissionIdentity(() -> |
| imm.setStylusWindowIdleTimeoutForTest(0)); |
| } |
| } |
| |
| /** |
| * Inject stylus events on top of an unfocused custom editor and verify handwriting is started |
| * and stylus handwriting window is displayed. |
| */ |
| @Test |
| public void testHandwriting_unfocusedCustomEditor() throws Exception { |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String focusedMarker = getTestMarker(); |
| final String unfocusedMarker = getTestMarker(); |
| final Pair<CustomEditorView, CustomEditorView> customEditorPair = |
| launchTestActivityWithCustomEditors(focusedMarker, unfocusedMarker); |
| final CustomEditorView unfocusedCustomEditor = customEditorPair.second; |
| |
| expectEvent(stream, editorMatcher("onStartInput", focusedMarker), TIMEOUT); |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", focusedMarker), |
| NOT_EXPECT_TIMEOUT); |
| |
| addVirtualStylusIdForTestSession(); |
| final int touchSlop = getTouchSlop(); |
| final int startX = unfocusedCustomEditor.getWidth() / 2; |
| final int startY = unfocusedCustomEditor.getHeight() / 2; |
| final int endX = startX + 2 * touchSlop; |
| final int endY = startY + 2 * touchSlop; |
| final int number = 5; |
| TestUtils.injectStylusDownEvent(unfocusedCustomEditor, startX, startY); |
| TestUtils.injectStylusMoveEvents(unfocusedCustomEditor, startX, startY, |
| endX, endY, number); |
| // Handwriting should already be initiated before ACTION_UP. |
| // unfocusedCustomEditor is focused and triggers onStartInput. |
| expectEvent(stream, editorMatcher("onStartInput", unfocusedMarker), TIMEOUT); |
| // Keyboard shouldn't show up. |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", unfocusedMarker), |
| NOT_EXPECT_TIMEOUT); |
| // Handwriting should start. |
| expectEvent( |
| stream, |
| editorMatcher("onStartStylusHandwriting", unfocusedMarker), |
| TIMEOUT); |
| |
| verifyStylusHandwritingWindowIsShown(stream, imeSession); |
| |
| // Verify that stylus move events are swallowed by the handwriting initiator once |
| // handwriting has been initiated and not dispatched to the view tree. |
| assertThat(unfocusedCustomEditor.mStylusMoveEventCount).isLessThan(number); |
| |
| TestUtils.injectStylusUpEvent(unfocusedCustomEditor, endX, endY); |
| } |
| } |
| |
| /** |
| * Inject stylus events on top of a focused custom editor that disables auto handwriting. |
| * |
| * @link InputMethodManager#startStylusHandwriting(View)} should not be called. |
| */ |
| @Test |
| public void testAutoHandwritingDisabled_customEditor() throws Exception { |
| try (MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String focusedMarker = getTestMarker(); |
| final String unfocusedMarker = getTestMarker(); |
| final Pair<CustomEditorView, CustomEditorView> customEditorPair = |
| launchTestActivityWithCustomEditors(focusedMarker, unfocusedMarker); |
| final CustomEditorView focusedCustomEditor = customEditorPair.first; |
| focusedCustomEditor.setAutoHandwritingEnabled(false); |
| |
| expectEvent(stream, editorMatcher("onStartInput", focusedMarker), TIMEOUT); |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", focusedMarker), |
| NOT_EXPECT_TIMEOUT); |
| |
| addVirtualStylusIdForTestSession(); |
| |
| injectStylusEventToEditorAndVerify( |
| focusedCustomEditor, stream, imeSession, focusedMarker, |
| false /* verifyHandwritingStart */, false, |
| false /* verifyHandwritingWindowIsShown */); |
| |
| // Verify that all stylus move events are dispatched to the view tree. |
| assertThat(focusedCustomEditor.mStylusMoveEventCount) |
| .isEqualTo(NUMBER_OF_INJECTED_EVENTS); |
| } |
| } |
| |
| private void injectStylusEventToEditorAndVerify( |
| View editor, ImeEventStream stream, MockImeSession imeSession, String marker, |
| boolean verifyHandwritingStart, boolean verifyHandwritingWindowIsShown, |
| boolean verifyHandwritingWindowNotShown) throws Exception { |
| final int touchSlop = getTouchSlop(); |
| final int startX = editor.getWidth() / 2; |
| final int startY = editor.getHeight() / 2; |
| final int endX = startX + 2 * touchSlop; |
| final int endY = startY + 2 * touchSlop; |
| TestUtils.injectStylusDownEvent(editor, startX, startY); |
| TestUtils.injectStylusMoveEvents( |
| editor, startX, startY, endX, endY, NUMBER_OF_INJECTED_EVENTS); |
| |
| // Handwriting should already be initiated before ACTION_UP. |
| // keyboard shouldn't show up. |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartInputView", marker), |
| NOT_EXPECT_TIMEOUT); |
| if (verifyHandwritingStart) { |
| // Handwriting should start |
| expectEvent( |
| stream, |
| editorMatcher("onStartStylusHandwriting", marker), |
| TIMEOUT); |
| } else { |
| // Handwriting should not start |
| notExpectEvent( |
| stream, |
| editorMatcher("onStartStylusHandwriting", marker), |
| NOT_EXPECT_TIMEOUT); |
| } |
| if (verifyHandwritingWindowIsShown) { |
| verifyStylusHandwritingWindowIsShown(stream, imeSession); |
| } else if (verifyHandwritingWindowNotShown) { |
| verifyStylusHandwritingWindowIsNotShown(stream, imeSession); |
| } |
| TestUtils.injectStylusUpEvent(editor, endX, endY); |
| } |
| |
| private EditText launchTestActivity(@NonNull String marker) { |
| return launchTestActivity(marker, getTestMarker()).first; |
| } |
| |
| private static String getTestMarker() { |
| return TEST_MARKER_PREFIX + "/" + SystemClock.elapsedRealtimeNanos(); |
| } |
| |
| private static int getTouchSlop() { |
| final Context context = InstrumentationRegistry.getInstrumentation().getContext(); |
| // Some tests require stylus movements to exceed the touch slop so that they are not |
| // interpreted as clicks. Other tests require the movements to exceed the handwriting slop |
| // to trigger handwriting initiation. Using the larger value allows all tests to pass. |
| return Math.max( |
| ViewConfiguration.get(context).getScaledTouchSlop(), |
| ViewConfiguration.get(context).getScaledHandwritingSlop()); |
| } |
| |
| private Pair<EditText, EditText> launchTestActivity(@NonNull String focusedMarker, |
| @NonNull String nonFocusedMarker) { |
| final AtomicReference<EditText> focusedEditTextRef = new AtomicReference<>(); |
| final AtomicReference<EditText> nonFocusedEditTextRef = new AtomicReference<>(); |
| TestActivity.startSync(activity -> { |
| final LinearLayout layout = new LinearLayout(activity); |
| layout.setOrientation(LinearLayout.VERTICAL); |
| // Adding some top padding tests that inject stylus event out of the view boundary. |
| layout.setPadding(0, 100, 0, 0); |
| |
| final EditText focusedEditText = new EditText(activity); |
| focusedEditText.setHint("focused editText"); |
| focusedEditText.setPrivateImeOptions(focusedMarker); |
| focusedEditText.requestFocus(); |
| focusedEditText.setAutoHandwritingEnabled(true); |
| focusedEditText.setHandwritingBoundsOffsets( |
| HANDWRITING_BOUNDS_OFFSET_PX, |
| HANDWRITING_BOUNDS_OFFSET_PX, |
| HANDWRITING_BOUNDS_OFFSET_PX, |
| HANDWRITING_BOUNDS_OFFSET_PX); |
| focusedEditTextRef.set(focusedEditText); |
| layout.addView(focusedEditText); |
| |
| final EditText nonFocusedEditText = new EditText(activity); |
| nonFocusedEditText.setPrivateImeOptions(nonFocusedMarker); |
| nonFocusedEditText.setHint("target editText"); |
| nonFocusedEditText.setAutoHandwritingEnabled(true); |
| nonFocusedEditTextRef.set(nonFocusedEditText); |
| nonFocusedEditText.setHandwritingBoundsOffsets( |
| HANDWRITING_BOUNDS_OFFSET_PX, |
| HANDWRITING_BOUNDS_OFFSET_PX, |
| HANDWRITING_BOUNDS_OFFSET_PX, |
| HANDWRITING_BOUNDS_OFFSET_PX); |
| // Leave margin between the EditTexts so that their extended handwriting bounds do not |
| // overlap. |
| LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( |
| ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); |
| layoutParams.topMargin = 3 * HANDWRITING_BOUNDS_OFFSET_PX; |
| layout.addView(nonFocusedEditText, layoutParams); |
| return layout; |
| }); |
| return new Pair<>(focusedEditTextRef.get(), nonFocusedEditTextRef.get()); |
| } |
| |
| private Pair<CustomEditorView, CustomEditorView> launchTestActivityWithCustomEditors( |
| @NonNull String focusedMarker, @NonNull String unfocusedMarker) { |
| final AtomicReference<CustomEditorView> focusedCustomEditorRef = new AtomicReference<>(); |
| final AtomicReference<CustomEditorView> unfocusedCustomEditorRef = new AtomicReference<>(); |
| TestActivity.startSync(activity -> { |
| final LinearLayout layout = new LinearLayout(activity); |
| layout.setOrientation(LinearLayout.VERTICAL); |
| // Add some top padding for tests that inject stylus event out of the view boundary. |
| layout.setPadding(0, 100, 0, 0); |
| |
| final CustomEditorView focusedCustomEditor = |
| new CustomEditorView(activity, focusedMarker, Color.RED); |
| focusedCustomEditor.setAutoHandwritingEnabled(true); |
| focusedCustomEditor.requestFocus(); |
| focusedCustomEditorRef.set(focusedCustomEditor); |
| layout.addView(focusedCustomEditor); |
| |
| final CustomEditorView unfocusedCustomEditor = |
| new CustomEditorView(activity, unfocusedMarker, Color.BLUE); |
| unfocusedCustomEditor.setAutoHandwritingEnabled(true); |
| unfocusedCustomEditorRef.set(unfocusedCustomEditor); |
| layout.addView(unfocusedCustomEditor); |
| |
| return layout; |
| }); |
| return new Pair<>(focusedCustomEditorRef.get(), unfocusedCustomEditorRef.get()); |
| } |
| |
| private View launchTestActivityWithDelegate( |
| @NonNull String editTextMarker, CountDownLatch delegateLatch, long delegateDelayMs) { |
| final AtomicReference<View> delegatorViewRef = new AtomicReference<>(); |
| TestActivity.startSync(activity -> { |
| final LinearLayout layout = new LinearLayout(activity); |
| layout.setOrientation(LinearLayout.VERTICAL); |
| |
| final View delegatorView = new View(activity); |
| delegatorViewRef.set(delegatorView); |
| delegatorView.setBackgroundColor(Color.GREEN); |
| delegatorView.setHandwritingDelegatorCallback( |
| () -> { |
| final EditText editText = new EditText(activity); |
| editText.setIsHandwritingDelegate(true); |
| editText.setPrivateImeOptions(editTextMarker); |
| editText.setHint("editText"); |
| layout.addView(editText); |
| editText.postDelayed(() -> { |
| editText.requestFocus(); |
| if (delegateLatch != null) { |
| delegateLatch.countDown(); |
| } |
| }, delegateDelayMs); |
| }); |
| |
| LinearLayout.LayoutParams layoutParams = |
| new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 40); |
| // Add space so that stylus motion on the delegate view is not within the edit text's |
| // extended handwriting bounds. |
| layoutParams.bottomMargin = 200; |
| layout.addView(delegatorView, layoutParams); |
| return layout; |
| }); |
| return delegatorViewRef.get(); |
| } |
| |
| private View launchTestActivityInExternalPackage( |
| @NonNull final String editTextMarker, final boolean setAllowedDelegatorPackage) { |
| final AtomicReference<View> delegateViewRef = new AtomicReference<>(); |
| TestActivity.startSync(activity -> { |
| final LinearLayout layout = new LinearLayout(activity); |
| layout.setOrientation(LinearLayout.VERTICAL); |
| |
| final View delegatorView = new View(activity); |
| delegateViewRef.set(delegatorView); |
| delegatorView.setBackgroundColor(Color.GREEN); |
| |
| delegatorView.setHandwritingDelegatorCallback(()-> { |
| // launch activity in a different package. |
| Intent intent = new Intent(Intent.ACTION_MAIN); |
| intent.setComponent(new ComponentName( |
| "android.view.inputmethod.ctstestapp", |
| "android.view.inputmethod.ctstestapp.MainActivity")); |
| intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| intent.putExtra(MockTestActivityUtil.EXTRA_KEY_PRIVATE_IME_OPTIONS, editTextMarker); |
| intent.putExtra(MockTestActivityUtil.EXTRA_HANDWRITING_DELEGATE, true); |
| activity.startActivity(intent); |
| }); |
| if (setAllowedDelegatorPackage) { |
| delegatorView.setAllowedHandwritingDelegatePackage( |
| "android.view.inputmethod.ctstestapp"); |
| } |
| layout.addView( |
| delegatorView, |
| new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 40)); |
| return layout; |
| }); |
| return delegateViewRef.get(); |
| } |
| |
| private boolean hasSupportedStylus() { |
| final InputManager im = mContext.getSystemService(InputManager.class); |
| for (int id : im.getInputDeviceIds()) { |
| InputDevice inputDevice = im.getInputDevice(id); |
| if (inputDevice != null && isStylusDevice(inputDevice)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private static boolean isStylusDevice(InputDevice inputDevice) { |
| return inputDevice.supportsSource(InputDevice.SOURCE_STYLUS) |
| || inputDevice.supportsSource(InputDevice.SOURCE_BLUETOOTH_STYLUS); |
| } |
| |
| private void addVirtualStylusIdForTestSession() { |
| SystemUtil.runWithShellPermissionIdentity(() -> { |
| mContext.getSystemService(InputMethodManager.class) |
| .addVirtualStylusIdForTestSession(); |
| }, Manifest.permission.TEST_INPUT_METHOD); |
| } |
| |
| private String getDefaultLauncher() throws Exception { |
| final String prefix = "Launcher: ComponentInfo{"; |
| final String postfix = "}"; |
| for (String s : |
| SystemUtil.runShellCommand("cmd shortcut get-default-launcher").split("\n")) { |
| if (s.startsWith(prefix) && s.endsWith(postfix)) { |
| return s.substring(prefix.length(), s.length() - postfix.length()); |
| } |
| } |
| throw new Exception("Default launcher not found"); |
| } |
| |
| private void setDefaultLauncher(String component) { |
| SystemUtil.runShellCommand("cmd package set-home-activity " + component); |
| } |
| |
| private static final class CustomEditorView extends View { |
| private final String mMarker; |
| private int mStylusMoveEventCount = 0; |
| |
| private CustomEditorView(Context context, @NonNull String marker, |
| @ColorInt int backgroundColor) { |
| super(context); |
| mMarker = marker; |
| setFocusable(true); |
| setFocusableInTouchMode(true); |
| setBackgroundColor(backgroundColor); |
| } |
| |
| @Override |
| public boolean onCheckIsTextEditor() { |
| return true; |
| } |
| |
| @Override |
| public InputConnection onCreateInputConnection(EditorInfo outAttrs) { |
| outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT; |
| outAttrs.privateImeOptions = mMarker; |
| return new NoOpInputConnection(); |
| } |
| |
| @Override |
| public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| // This View needs a valid size to be focusable. |
| setMeasuredDimension(300, 100); |
| } |
| |
| @Override |
| public boolean onTouchEvent(MotionEvent event) { |
| if (event.getToolType(event.getActionIndex()) == MotionEvent.TOOL_TYPE_STYLUS) { |
| if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { |
| // Return true to receive ACTION_MOVE events. |
| return true; |
| } else if (event.getActionMasked() == MotionEvent.ACTION_MOVE) { |
| mStylusMoveEventCount++; |
| } |
| } |
| return super.onTouchEvent(event); |
| } |
| } |
| } |