diff options
| -rw-r--r-- | services/inputflinger/reader/mapper/TouchInputMapper.cpp | 63 | ||||
| -rw-r--r-- | services/inputflinger/reader/mapper/TouchInputMapper.h | 14 | ||||
| -rw-r--r-- | services/inputflinger/tests/InputReader_test.cpp | 698 | ||||
| -rw-r--r-- | services/inputflinger/tests/TestInputListener.cpp | 14 | ||||
| -rw-r--r-- | services/inputflinger/tests/TestInputListener.h | 6 | ||||
| -rw-r--r-- | services/inputflinger/tests/TestInputListenerMatchers.h | 9 | ||||
| -rw-r--r-- | services/inputflinger/tests/UinputDevice.cpp | 34 | ||||
| -rw-r--r-- | services/inputflinger/tests/UinputDevice.h | 29 |
8 files changed, 756 insertions, 111 deletions
diff --git a/services/inputflinger/reader/mapper/TouchInputMapper.cpp b/services/inputflinger/reader/mapper/TouchInputMapper.cpp index bf73ce5db8..8bae650ff7 100644 --- a/services/inputflinger/reader/mapper/TouchInputMapper.cpp +++ b/services/inputflinger/reader/mapper/TouchInputMapper.cpp @@ -21,6 +21,7 @@ #include "TouchInputMapper.h" #include <ftl/enum.h> +#include <input/PrintTools.h> #include "CursorButtonAccumulator.h" #include "CursorScrollAccumulator.h" @@ -31,13 +32,6 @@ namespace android { // --- Constants --- -// Maximum amount of latency to add to touch events while waiting for data from an -// external stylus. -static constexpr nsecs_t EXTERNAL_STYLUS_DATA_TIMEOUT = ms2ns(72); - -// Maximum amount of time to wait on touch data before pushing out new pressure data. -static constexpr nsecs_t TOUCH_DATA_TIMEOUT = ms2ns(20); - // Artificial latency on synthetic events created from stylus data without corresponding touch // data. static constexpr nsecs_t STYLUS_DATA_LATENCY = ms2ns(10); @@ -258,9 +252,12 @@ void TouchInputMapper::dump(std::string& dump) { dump += INDENT3 "Stylus Fusion:\n"; dump += StringPrintf(INDENT4 "ExternalStylusConnected: %s\n", toString(mExternalStylusConnected)); - dump += StringPrintf(INDENT4 "External Stylus ID: %" PRId64 "\n", mExternalStylusId); + dump += StringPrintf(INDENT4 "Fused External Stylus Pointer ID: %s\n", + toString(mFusedStylusPointerId).c_str()); dump += StringPrintf(INDENT4 "External Stylus Data Timeout: %" PRId64 "\n", mExternalStylusFusionTimeout); + dump += StringPrintf(INDENT4 " External Stylus Buttons Applied: 0x%08x", + mExternalStylusButtonsApplied); dump += INDENT3 "External Stylus State:\n"; dumpStylusState(dump, mExternalStylusState); @@ -1417,9 +1414,10 @@ std::list<NotifyArgs> TouchInputMapper::reset(nsecs_t when) { void TouchInputMapper::resetExternalStylus() { mExternalStylusState.clear(); - mExternalStylusId = -1; + mFusedStylusPointerId.reset(); mExternalStylusFusionTimeout = LLONG_MAX; mExternalStylusDataPending = false; + mExternalStylusButtonsApplied = 0; } void TouchInputMapper::clearStylusDataPendingFlags() { @@ -1693,8 +1691,17 @@ bool TouchInputMapper::isTouchScreen() { } void TouchInputMapper::applyExternalStylusButtonState(nsecs_t when) { - if (mDeviceMode == DeviceMode::DIRECT && hasExternalStylus() && mExternalStylusId != -1) { - mCurrentRawState.buttonState |= mExternalStylusState.buttons; + if (mDeviceMode == DeviceMode::DIRECT && hasExternalStylus()) { + // If any of the external buttons are already pressed by the touch device, ignore them. + const int32_t pressedButtons = ~mCurrentRawState.buttonState & mExternalStylusState.buttons; + const int32_t releasedButtons = + mExternalStylusButtonsApplied & ~mExternalStylusState.buttons; + + mCurrentRawState.buttonState |= pressedButtons; + mCurrentRawState.buttonState &= ~releasedButtons; + + mExternalStylusButtonsApplied |= pressedButtons; + mExternalStylusButtonsApplied &= ~releasedButtons; } } @@ -1702,17 +1709,18 @@ void TouchInputMapper::applyExternalStylusTouchState(nsecs_t when) { CookedPointerData& currentPointerData = mCurrentCookedState.cookedPointerData; const CookedPointerData& lastPointerData = mLastCookedState.cookedPointerData; - if (mExternalStylusId != -1 && currentPointerData.isTouching(mExternalStylusId)) { + if (mFusedStylusPointerId && currentPointerData.isTouching(*mFusedStylusPointerId)) { float pressure = mExternalStylusState.pressure; - if (pressure == 0.0f && lastPointerData.isTouching(mExternalStylusId)) { - const PointerCoords& coords = lastPointerData.pointerCoordsForId(mExternalStylusId); + if (pressure == 0.0f && lastPointerData.isTouching(*mFusedStylusPointerId)) { + const PointerCoords& coords = + lastPointerData.pointerCoordsForId(*mFusedStylusPointerId); pressure = coords.getAxisValue(AMOTION_EVENT_AXIS_PRESSURE); } - PointerCoords& coords = currentPointerData.editPointerCoordsWithId(mExternalStylusId); + PointerCoords& coords = currentPointerData.editPointerCoordsWithId(*mFusedStylusPointerId); coords.setAxisValue(AMOTION_EVENT_AXIS_PRESSURE, pressure); PointerProperties& properties = - currentPointerData.editPointerPropertiesWithId(mExternalStylusId); + currentPointerData.editPointerPropertiesWithId(*mFusedStylusPointerId); if (mExternalStylusState.toolType != AMOTION_EVENT_TOOL_TYPE_UNKNOWN) { properties.toolType = mExternalStylusState.toolType; } @@ -1729,10 +1737,11 @@ bool TouchInputMapper::assignExternalStylusId(const RawState& state, bool timeou if (initialDown) { if (mExternalStylusState.pressure != 0.0f) { ALOGD_IF(DEBUG_STYLUS_FUSION, "Have both stylus and touch data, beginning fusion"); - mExternalStylusId = state.rawPointerData.touchingIdBits.firstMarkedBit(); + mFusedStylusPointerId = state.rawPointerData.touchingIdBits.firstMarkedBit(); } else if (timeout) { ALOGD_IF(DEBUG_STYLUS_FUSION, "Timeout expired, assuming touch is not a stylus."); - resetExternalStylus(); + mFusedStylusPointerId.reset(); + mExternalStylusFusionTimeout = LLONG_MAX; } else { if (mExternalStylusFusionTimeout == LLONG_MAX) { mExternalStylusFusionTimeout = state.when + EXTERNAL_STYLUS_DATA_TIMEOUT; @@ -1746,9 +1755,10 @@ bool TouchInputMapper::assignExternalStylusId(const RawState& state, bool timeou } // Check if the stylus pointer has gone up. - if (mExternalStylusId != -1 && !state.rawPointerData.touchingIdBits.hasBit(mExternalStylusId)) { + if (mFusedStylusPointerId && + !state.rawPointerData.touchingIdBits.hasBit(*mFusedStylusPointerId)) { ALOGD_IF(DEBUG_STYLUS_FUSION, "Stylus pointer is going up"); - mExternalStylusId = -1; + mFusedStylusPointerId.reset(); } return false; @@ -1763,7 +1773,7 @@ std::list<NotifyArgs> TouchInputMapper::timeoutExpired(nsecs_t when) { out += dispatchPointerGestures(when, readTime, 0 /*policyFlags*/, true /*isTimeout*/); } } else if (mDeviceMode == DeviceMode::DIRECT) { - if (mExternalStylusFusionTimeout < when) { + if (mExternalStylusFusionTimeout <= when) { out += processRawTouches(true /*timeout*/); } else if (mExternalStylusFusionTimeout != LLONG_MAX) { getContext()->requestTimeoutAtTime(mExternalStylusFusionTimeout); @@ -1774,11 +1784,14 @@ std::list<NotifyArgs> TouchInputMapper::timeoutExpired(nsecs_t when) { std::list<NotifyArgs> TouchInputMapper::updateExternalStylusState(const StylusState& state) { std::list<NotifyArgs> out; + const bool buttonsChanged = mExternalStylusState.buttons != state.buttons; mExternalStylusState.copyFrom(state); - if (mExternalStylusId != -1 || mExternalStylusFusionTimeout != LLONG_MAX) { - // We're either in the middle of a fused stream of data or we're waiting on data before - // dispatching the initial down, so go ahead and dispatch now that we have fresh stylus - // data. + if (mFusedStylusPointerId || mExternalStylusFusionTimeout != LLONG_MAX || buttonsChanged) { + // The following three cases are handled here: + // - We're in the middle of a fused stream of data; + // - We're waiting on external stylus data before dispatching the initial down; or + // - Only the button state, which is not reported through a specific pointer, has changed. + // Go ahead and dispatch now that we have fresh stylus data. mExternalStylusDataPending = true; out += processRawTouches(false /*timeout*/); } diff --git a/services/inputflinger/reader/mapper/TouchInputMapper.h b/services/inputflinger/reader/mapper/TouchInputMapper.h index c20f28bc19..fba7b79415 100644 --- a/services/inputflinger/reader/mapper/TouchInputMapper.h +++ b/services/inputflinger/reader/mapper/TouchInputMapper.h @@ -27,6 +27,13 @@ namespace android { +// Maximum amount of latency to add to touch events while waiting for data from an +// external stylus. +static constexpr nsecs_t EXTERNAL_STYLUS_DATA_TIMEOUT = ms2ns(72); + +// Maximum amount of time to wait on touch data before pushing out new pressure data. +static constexpr nsecs_t TOUCH_DATA_TIMEOUT = ms2ns(20); + /* Raw axis information from the driver. */ struct RawPointerAxes { RawAbsoluteAxisInfo x{}; @@ -355,9 +362,14 @@ protected: // State provided by an external stylus StylusState mExternalStylusState; - int64_t mExternalStylusId; + // If an external stylus is capable of reporting pointer-specific data like pressure, we will + // attempt to fuse the pointer data reported by the stylus to the first touch pointer. This is + // the id of the pointer to which the external stylus data is fused. + std::optional<uint32_t> mFusedStylusPointerId; nsecs_t mExternalStylusFusionTimeout; bool mExternalStylusDataPending; + // A subset of the buttons in mCurrentRawState that came from an external stylus. + int32_t mExternalStylusButtonsApplied; // True if we sent a HOVER_ENTER event. bool mSentHoverEnter; diff --git a/services/inputflinger/tests/InputReader_test.cpp b/services/inputflinger/tests/InputReader_test.cpp index 879d36e6fd..8a361fa44c 100644 --- a/services/inputflinger/tests/InputReader_test.cpp +++ b/services/inputflinger/tests/InputReader_test.cpp @@ -1349,6 +1349,8 @@ protected: int32_t mGlobalMetaState; bool mUpdateGlobalMetaStateWasCalled; int32_t mGeneration; + std::optional<nsecs_t> mRequestedTimeout; + std::vector<InputDeviceInfo> mExternalStylusDevices; public: FakeInputReaderContext(InputReader* reader) @@ -1382,6 +1384,29 @@ protected: mGeneration = ContextImpl::bumpGeneration(); return mGeneration; } + + void requestTimeoutAtTime(nsecs_t when) override { mRequestedTimeout = when; } + + void assertTimeoutWasRequested(nsecs_t when) { + ASSERT_TRUE(mRequestedTimeout) << "Expected timeout at time " << when + << " but there was no timeout requested."; + ASSERT_EQ(when, *mRequestedTimeout); + mRequestedTimeout.reset(); + } + + void assertTimeoutWasNotRequested() { + ASSERT_FALSE(mRequestedTimeout) << "Expected no timeout to have been requested," + " but one was requested at time " + << *mRequestedTimeout; + } + + void getExternalStylusDevices(std::vector<InputDeviceInfo>& outDevices) override { + outDevices = mExternalStylusDevices; + } + + void setExternalStylusDevices(std::vector<InputDeviceInfo>&& devices) { + mExternalStylusDevices = devices; + } } mFakeContext; friend class InputReaderTest; @@ -2504,7 +2529,8 @@ TEST_F(InputReaderIntegrationTest, SendsGearDownAndUpToInputListener) { ASSERT_EQ(BTN_GEAR_UP, keyArgs.scanCode); } -// --- TouchProcessTest --- +// --- TouchIntegrationTest --- + class TouchIntegrationTest : public InputReaderIntegrationTest { protected: const std::string UNIQUE_ID = "local:0"; @@ -2817,108 +2843,294 @@ TEST_F(TouchIntegrationTest, NotifiesPolicyWhenStylusGestureStarted) { ASSERT_NO_FATAL_FAILURE(mFakePolicy->assertStylusGestureNotified(mDeviceInfo.getId())); } -TEST_F(TouchIntegrationTest, StylusButtonsGenerateKeyEvents) { - mDevice->sendKey(BTN_STYLUS, 1); - mDevice->sendSync(); - ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyKeyWasCalled( +// --- StylusButtonIntegrationTest --- + +// Verify the behavior of button presses reported by various kinds of styluses, including buttons +// reported by the touchscreen's device, by a fused external stylus, and by an un-fused external +// stylus. +template <typename UinputStylusDevice> +class StylusButtonIntegrationTest : public TouchIntegrationTest { +protected: + void SetUp() override { +#if !defined(__ANDROID__) + GTEST_SKIP(); +#endif + TouchIntegrationTest::SetUp(); + mTouchscreen = mDevice.get(); + mTouchscreenInfo = mDeviceInfo; + + setUpStylusDevice(); + } + + UinputStylusDevice* mStylus{nullptr}; + InputDeviceInfo mStylusInfo{}; + + UinputTouchScreen* mTouchscreen{nullptr}; + InputDeviceInfo mTouchscreenInfo{}; + +private: + // When we are attempting to test stylus button events that are sent from the touchscreen, + // use the same Uinput device for the touchscreen and the stylus. + template <typename T = UinputStylusDevice> + std::enable_if_t<std::is_same_v<UinputTouchScreen, T>, void> setUpStylusDevice() { + mStylus = mDevice.get(); + mStylusInfo = mDeviceInfo; + } + + // When we are attempting to stylus buttons from an external stylus being merged with touches + // from a touchscreen, create a new Uinput device through which stylus buttons can be injected. + template <typename T = UinputStylusDevice> + std::enable_if_t<!std::is_same_v<UinputTouchScreen, T>, void> setUpStylusDevice() { + mStylusDeviceLifecycleTracker = createUinputDevice<T>(); + mStylus = mStylusDeviceLifecycleTracker.get(); + ASSERT_NO_FATAL_FAILURE(mFakePolicy->assertInputDevicesChanged()); + ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyConfigurationChangedWasCalled()); + const auto info = findDeviceByName(mStylus->getName()); + ASSERT_TRUE(info); + mStylusInfo = *info; + } + + std::unique_ptr<UinputStylusDevice> mStylusDeviceLifecycleTracker{}; + + // Hide the base class's device to expose it with a different name for readability. + using TouchIntegrationTest::mDevice; + using TouchIntegrationTest::mDeviceInfo; +}; + +using StylusButtonIntegrationTestTypes = + ::testing::Types<UinputTouchScreen, UinputExternalStylus, UinputExternalStylusWithPressure>; +TYPED_TEST_SUITE(StylusButtonIntegrationTest, StylusButtonIntegrationTestTypes); + +TYPED_TEST(StylusButtonIntegrationTest, StylusButtonsGenerateKeyEvents) { + const auto stylusId = TestFixture::mStylusInfo.getId(); + + TestFixture::mStylus->pressKey(BTN_STYLUS); + ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyKeyWasCalled( AllOf(WithKeyAction(AKEY_EVENT_ACTION_DOWN), WithSource(AINPUT_SOURCE_KEYBOARD), - WithKeyCode(AKEYCODE_STYLUS_BUTTON_PRIMARY)))); + WithKeyCode(AKEYCODE_STYLUS_BUTTON_PRIMARY), WithDeviceId(stylusId)))); - mDevice->sendKey(BTN_STYLUS, 0); - mDevice->sendSync(); - ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyKeyWasCalled( + TestFixture::mStylus->releaseKey(BTN_STYLUS); + ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyKeyWasCalled( AllOf(WithKeyAction(AKEY_EVENT_ACTION_UP), WithSource(AINPUT_SOURCE_KEYBOARD), - WithKeyCode(AKEYCODE_STYLUS_BUTTON_PRIMARY)))); + WithKeyCode(AKEYCODE_STYLUS_BUTTON_PRIMARY), WithDeviceId(stylusId)))); } -TEST_F(TouchIntegrationTest, StylusButtonsSurroundingTouchGesture) { - const Point centerPoint = mDevice->getCenterPoint(); +TYPED_TEST(StylusButtonIntegrationTest, StylusButtonsSurroundingTouchGesture) { + const Point centerPoint = TestFixture::mTouchscreen->getCenterPoint(); + const auto touchscreenId = TestFixture::mTouchscreenInfo.getId(); + const auto stylusId = TestFixture::mStylusInfo.getId(); // Press the stylus button. - mDevice->sendKey(BTN_STYLUS, 1); - mDevice->sendSync(); - ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyKeyWasCalled( + TestFixture::mStylus->pressKey(BTN_STYLUS); + ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyKeyWasCalled( AllOf(WithKeyAction(AKEY_EVENT_ACTION_DOWN), WithSource(AINPUT_SOURCE_KEYBOARD), - WithKeyCode(AKEYCODE_STYLUS_BUTTON_PRIMARY)))); + WithKeyCode(AKEYCODE_STYLUS_BUTTON_PRIMARY), WithDeviceId(stylusId)))); // Start and finish a stylus gesture. - mDevice->sendSlot(FIRST_SLOT); - mDevice->sendTrackingId(FIRST_TRACKING_ID); - mDevice->sendToolType(MT_TOOL_PEN); - mDevice->sendDown(centerPoint); - mDevice->sendSync(); - ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled( + TestFixture::mTouchscreen->sendSlot(FIRST_SLOT); + TestFixture::mTouchscreen->sendTrackingId(FIRST_TRACKING_ID); + TestFixture::mTouchscreen->sendToolType(MT_TOOL_PEN); + TestFixture::mTouchscreen->sendDown(centerPoint); + TestFixture::mTouchscreen->sendSync(); + ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyMotionWasCalled( AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), - WithButtonState(AMOTION_EVENT_BUTTON_STYLUS_PRIMARY)))); - ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled( + WithButtonState(AMOTION_EVENT_BUTTON_STYLUS_PRIMARY), + WithDeviceId(touchscreenId)))); + ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyMotionWasCalled( AllOf(WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_PRESS), WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), - WithButtonState(AMOTION_EVENT_BUTTON_STYLUS_PRIMARY)))); + WithButtonState(AMOTION_EVENT_BUTTON_STYLUS_PRIMARY), + WithDeviceId(touchscreenId)))); - mDevice->sendTrackingId(INVALID_TRACKING_ID); - mDevice->sendSync(); - ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled( + TestFixture::mTouchscreen->sendTrackingId(INVALID_TRACKING_ID); + TestFixture::mTouchscreen->sendSync(); + ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyMotionWasCalled( AllOf(WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_RELEASE), - WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0)))); - ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled( + WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0), + WithDeviceId(touchscreenId)))); + ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyMotionWasCalled( AllOf(WithMotionAction(AMOTION_EVENT_ACTION_UP), - WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0)))); + WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0), + WithDeviceId(touchscreenId)))); // Release the stylus button. - mDevice->sendKey(BTN_STYLUS, 0); - mDevice->sendSync(); - ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyKeyWasCalled( + TestFixture::mStylus->releaseKey(BTN_STYLUS); + ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyKeyWasCalled( AllOf(WithKeyAction(AKEY_EVENT_ACTION_UP), WithSource(AINPUT_SOURCE_KEYBOARD), - WithKeyCode(AKEYCODE_STYLUS_BUTTON_PRIMARY)))); + WithKeyCode(AKEYCODE_STYLUS_BUTTON_PRIMARY), WithDeviceId(stylusId)))); } -TEST_F(TouchIntegrationTest, StylusButtonsWithinTouchGesture) { - const Point centerPoint = mDevice->getCenterPoint(); +TYPED_TEST(StylusButtonIntegrationTest, StylusButtonsWithinTouchGesture) { + const Point centerPoint = TestFixture::mTouchscreen->getCenterPoint(); + const auto touchscreenId = TestFixture::mTouchscreenInfo.getId(); + const auto stylusId = TestFixture::mStylusInfo.getId(); // Start a stylus gesture. - mDevice->sendSlot(FIRST_SLOT); - mDevice->sendTrackingId(FIRST_TRACKING_ID); - mDevice->sendToolType(MT_TOOL_PEN); - mDevice->sendDown(centerPoint); - mDevice->sendSync(); - ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled( + TestFixture::mTouchscreen->sendSlot(FIRST_SLOT); + TestFixture::mTouchscreen->sendTrackingId(FIRST_TRACKING_ID); + TestFixture::mTouchscreen->sendToolType(MT_TOOL_PEN); + TestFixture::mTouchscreen->sendDown(centerPoint); + TestFixture::mTouchscreen->sendSync(); + ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyMotionWasCalled( AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), - WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0)))); + WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0), + WithDeviceId(touchscreenId)))); // Press and release a stylus button. Each change in button state also generates a MOVE event. - mDevice->sendKey(BTN_STYLUS, 1); - mDevice->sendSync(); - ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyKeyWasCalled( + TestFixture::mStylus->pressKey(BTN_STYLUS); + ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyKeyWasCalled( AllOf(WithKeyAction(AKEY_EVENT_ACTION_DOWN), WithSource(AINPUT_SOURCE_KEYBOARD), - WithKeyCode(AKEYCODE_STYLUS_BUTTON_PRIMARY)))); - ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled( + WithKeyCode(AKEYCODE_STYLUS_BUTTON_PRIMARY), WithDeviceId(stylusId)))); + ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyMotionWasCalled( AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE), WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), - WithButtonState(AMOTION_EVENT_BUTTON_STYLUS_PRIMARY)))); - ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled( + WithButtonState(AMOTION_EVENT_BUTTON_STYLUS_PRIMARY), + WithDeviceId(touchscreenId)))); + ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyMotionWasCalled( AllOf(WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_PRESS), WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), - WithButtonState(AMOTION_EVENT_BUTTON_STYLUS_PRIMARY)))); + WithButtonState(AMOTION_EVENT_BUTTON_STYLUS_PRIMARY), + WithDeviceId(touchscreenId)))); - mDevice->sendKey(BTN_STYLUS, 0); - mDevice->sendSync(); - ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyKeyWasCalled( + TestFixture::mStylus->releaseKey(BTN_STYLUS); + ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyKeyWasCalled( AllOf(WithKeyAction(AKEY_EVENT_ACTION_UP), WithSource(AINPUT_SOURCE_KEYBOARD), - WithKeyCode(AKEYCODE_STYLUS_BUTTON_PRIMARY)))); - ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled( + WithKeyCode(AKEYCODE_STYLUS_BUTTON_PRIMARY), WithDeviceId(stylusId)))); + ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyMotionWasCalled( AllOf(WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_RELEASE), - WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0)))); - ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled( + WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0), + WithDeviceId(touchscreenId)))); + ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyMotionWasCalled( AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE), - WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0)))); + WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0), + WithDeviceId(touchscreenId)))); // Finish the stylus gesture. + TestFixture::mTouchscreen->sendTrackingId(INVALID_TRACKING_ID); + TestFixture::mTouchscreen->sendSync(); + ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyMotionWasCalled( + AllOf(WithMotionAction(AMOTION_EVENT_ACTION_UP), + WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0), + WithDeviceId(touchscreenId)))); +} + +// --- ExternalStylusIntegrationTest --- + +// Verify the behavior of an external stylus. An external stylus can report pressure or button +// data independently of the touchscreen, which is then sent as a MotionEvent as part of an +// ongoing stylus gesture that is being emitted by the touchscreen. +using ExternalStylusIntegrationTest = TouchIntegrationTest; + +TEST_F(ExternalStylusIntegrationTest, FusedExternalStylusPressureReported) { + const Point centerPoint = mDevice->getCenterPoint(); + + // Create an external stylus capable of reporting pressure data that + // should be fused with a touch pointer. + std::unique_ptr<UinputExternalStylusWithPressure> stylus = + createUinputDevice<UinputExternalStylusWithPressure>(); + ASSERT_NO_FATAL_FAILURE(mFakePolicy->assertInputDevicesChanged()); + ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyConfigurationChangedWasCalled()); + const auto stylusInfo = findDeviceByName(stylus->getName()); + ASSERT_TRUE(stylusInfo); + + ASSERT_EQ(AINPUT_SOURCE_STYLUS | AINPUT_SOURCE_KEYBOARD, stylusInfo->getSources()); + + const auto touchscreenId = mDeviceInfo.getId(); + + // Set a pressure value on the stylus. It doesn't generate any events. + const auto& RAW_PRESSURE_MAX = UinputExternalStylusWithPressure::RAW_PRESSURE_MAX; + stylus->setPressure(100); + ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasNotCalled()); + + // Start a finger gesture, and ensure it shows up as stylus gesture + // with the pressure set by the external stylus. + mDevice->sendSlot(FIRST_SLOT); + mDevice->sendTrackingId(FIRST_TRACKING_ID); + mDevice->sendToolType(MT_TOOL_FINGER); + mDevice->sendDown(centerPoint); + mDevice->sendSync(); + ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled( + AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), + WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0), + WithDeviceId(touchscreenId), WithPressure(100.f / RAW_PRESSURE_MAX)))); + + // Change the pressure on the external stylus, and ensure the touchscreen generates a MOVE + // event with the updated pressure. + stylus->setPressure(200); + ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled( + AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE), + WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0), + WithDeviceId(touchscreenId), WithPressure(200.f / RAW_PRESSURE_MAX)))); + + // The external stylus did not generate any events. + ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasNotCalled()); + ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyKeyWasNotCalled()); +} + +TEST_F(ExternalStylusIntegrationTest, FusedExternalStylusPressureNotReported) { + const Point centerPoint = mDevice->getCenterPoint(); + + // Create an external stylus capable of reporting pressure data that + // should be fused with a touch pointer. + std::unique_ptr<UinputExternalStylusWithPressure> stylus = + createUinputDevice<UinputExternalStylusWithPressure>(); + ASSERT_NO_FATAL_FAILURE(mFakePolicy->assertInputDevicesChanged()); + ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyConfigurationChangedWasCalled()); + const auto stylusInfo = findDeviceByName(stylus->getName()); + ASSERT_TRUE(stylusInfo); + + ASSERT_EQ(AINPUT_SOURCE_STYLUS | AINPUT_SOURCE_KEYBOARD, stylusInfo->getSources()); + + const auto touchscreenId = mDeviceInfo.getId(); + + // Set a pressure value of 0 on the stylus. It doesn't generate any events. + const auto& RAW_PRESSURE_MAX = UinputExternalStylusWithPressure::RAW_PRESSURE_MAX; + stylus->setPressure(0); + + // Start a finger gesture. The touch device will withhold generating any touches for + // up to 72 milliseconds while waiting for pressure data from the external stylus. + mDevice->sendSlot(FIRST_SLOT); + mDevice->sendTrackingId(FIRST_TRACKING_ID); + mDevice->sendToolType(MT_TOOL_FINGER); + mDevice->sendDown(centerPoint); + auto waitUntil = std::chrono::system_clock::now() + + std::chrono::milliseconds(ns2ms(EXTERNAL_STYLUS_DATA_TIMEOUT)); + mDevice->sendSync(); + ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasNotCalled(waitUntil)); + + // Since the external stylus did not report a pressure value within the timeout, + // it shows up as a finger pointer. + ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled( + AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), + WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER), WithDeviceId(touchscreenId), + WithPressure(1.f)))); + + // Change the pressure on the external stylus. Since the pressure was not present at the start + // of the gesture, it is ignored for now. + stylus->setPressure(200); + ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasNotCalled()); + + // Finish the finger gesture. mDevice->sendTrackingId(INVALID_TRACKING_ID); mDevice->sendSync(); ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled( AllOf(WithMotionAction(AMOTION_EVENT_ACTION_UP), - WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0)))); + WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)))); + + // Start a new gesture. Since we have a valid pressure value, it shows up as a stylus. + mDevice->sendTrackingId(FIRST_TRACKING_ID); + mDevice->sendToolType(MT_TOOL_FINGER); + mDevice->sendDown(centerPoint); + mDevice->sendSync(); + ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled( + AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), + WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0), + WithDeviceId(touchscreenId), WithPressure(200.f / RAW_PRESSURE_MAX)))); + + // The external stylus did not generate any events. + ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasNotCalled()); + ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyKeyWasNotCalled()); } // --- InputDeviceTest --- @@ -3372,6 +3584,16 @@ protected: mReader->loopOnce(); } + std::list<NotifyArgs> handleTimeout(InputMapper& mapper, nsecs_t when) { + std::list<NotifyArgs> generatedArgs = mapper.timeoutExpired(when); + for (const NotifyArgs& args : generatedArgs) { + mFakeListener->notify(args); + } + // Loop the reader to flush the input listener queue. + mReader->loopOnce(); + return generatedArgs; + } + static void assertMotionRange(const InputDeviceInfo& info, int32_t axis, uint32_t source, float min, float max, float flat, float fuzz) { const InputDeviceInfo::MotionRange* range = info.getMotionRange(axis, source); @@ -7566,6 +7788,360 @@ TEST_F(TouchDisplayProjectionTest, EmitsTouchDownAfterEnteringPhysicalDisplay) { } } +// --- ExternalStylusFusionTest --- + +class ExternalStylusFusionTest : public SingleTouchInputMapperTest { +public: + SingleTouchInputMapper& initializeInputMapperWithExternalStylus() { + addConfigurationProperty("touch.deviceType", "touchScreen"); + prepareDisplay(DISPLAY_ORIENTATION_0); + prepareButtons(); + prepareAxes(POSITION); + auto& mapper = addMapperAndConfigure<SingleTouchInputMapper>(); + + mStylusState.when = ARBITRARY_TIME; + mStylusState.pressure = 0.f; + mStylusState.toolType = AMOTION_EVENT_TOOL_TYPE_STYLUS; + mReader->getContext()->setExternalStylusDevices({mExternalStylusDeviceInfo}); + configureDevice(InputReaderConfiguration::CHANGE_EXTERNAL_STYLUS_PRESENCE); + processExternalStylusState(mapper); + return mapper; + } + + std::list<NotifyArgs> processExternalStylusState(InputMapper& mapper) { + std::list<NotifyArgs> generatedArgs = mapper.updateExternalStylusState(mStylusState); + for (const NotifyArgs& args : generatedArgs) { + mFakeListener->notify(args); + } + // Loop the reader to flush the input listener queue. + mReader->loopOnce(); + return generatedArgs; + } + +protected: + StylusState mStylusState{}; + static constexpr uint32_t EXPECTED_SOURCE = + AINPUT_SOURCE_TOUCHSCREEN | AINPUT_SOURCE_BLUETOOTH_STYLUS; + + void testStartFusedStylusGesture(SingleTouchInputMapper& mapper) { + auto toolTypeSource = + AllOf(WithSource(EXPECTED_SOURCE), WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS)); + + // The first pointer is withheld. + processDown(mapper, 100, 200); + processSync(mapper); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled()); + ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasRequested( + ARBITRARY_TIME + EXTERNAL_STYLUS_DATA_TIMEOUT)); + + // The external stylus reports pressure. The withheld finger pointer is released as a + // stylus. + mStylusState.pressure = 1.f; + processExternalStylusState(mapper); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled( + AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_DOWN)))); + ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested()); + + // Subsequent pointer events are not withheld. + processMove(mapper, 101, 201); + processSync(mapper); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled( + AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_MOVE)))); + + ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested()); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled()); + } + + void testSuccessfulFusionGesture(SingleTouchInputMapper& mapper) { + ASSERT_NO_FATAL_FAILURE(testStartFusedStylusGesture(mapper)); + + // Releasing the touch pointer ends the gesture. + processUp(mapper); + processSync(mapper); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled( + AllOf(WithMotionAction(AMOTION_EVENT_ACTION_UP), WithSource(EXPECTED_SOURCE), + WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS)))); + + mStylusState.pressure = 0.f; + processExternalStylusState(mapper); + ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested()); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled()); + } + + void testUnsuccessfulFusionGesture(SingleTouchInputMapper& mapper) { + auto toolTypeSource = + AllOf(WithSource(EXPECTED_SOURCE), WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)); + + // The first pointer is withheld when an external stylus is connected, + // and a timeout is requested. + processDown(mapper, 100, 200); + processSync(mapper); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled()); + ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasRequested( + ARBITRARY_TIME + EXTERNAL_STYLUS_DATA_TIMEOUT)); + + // If the timeout expires early, it is requested again. + handleTimeout(mapper, ARBITRARY_TIME + 1); + ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasRequested( + ARBITRARY_TIME + EXTERNAL_STYLUS_DATA_TIMEOUT)); + + // When the timeout expires, the withheld touch is released as a finger pointer. + handleTimeout(mapper, ARBITRARY_TIME + EXTERNAL_STYLUS_DATA_TIMEOUT); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled( + AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_DOWN)))); + + // Subsequent pointer events are not withheld. + processMove(mapper, 101, 201); + processSync(mapper); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled( + AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_MOVE)))); + processUp(mapper); + processSync(mapper); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled( + AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_UP)))); + + ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested()); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled()); + } + +private: + InputDeviceInfo mExternalStylusDeviceInfo{}; +}; + +TEST_F(ExternalStylusFusionTest, UsesBluetoothStylusSource) { + SingleTouchInputMapper& mapper = initializeInputMapperWithExternalStylus(); + ASSERT_EQ(EXPECTED_SOURCE, mapper.getSources()); +} + +TEST_F(ExternalStylusFusionTest, UnsuccessfulFusion) { + SingleTouchInputMapper& mapper = initializeInputMapperWithExternalStylus(); + ASSERT_NO_FATAL_FAILURE(testUnsuccessfulFusionGesture(mapper)); +} + +TEST_F(ExternalStylusFusionTest, SuccessfulFusion_TouchFirst) { + SingleTouchInputMapper& mapper = initializeInputMapperWithExternalStylus(); + ASSERT_NO_FATAL_FAILURE(testSuccessfulFusionGesture(mapper)); +} + +// Test a successful stylus fusion gesture where the pressure is reported by the external +// before the touch is reported by the touchscreen. +TEST_F(ExternalStylusFusionTest, SuccessfulFusion_PressureFirst) { + SingleTouchInputMapper& mapper = initializeInputMapperWithExternalStylus(); + auto toolTypeSource = + AllOf(WithSource(EXPECTED_SOURCE), WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS)); + + // The external stylus reports pressure first. It is ignored for now. + mStylusState.pressure = 1.f; + processExternalStylusState(mapper); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled()); + ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested()); + + // When the touch goes down afterwards, it is reported as a stylus pointer. + processDown(mapper, 100, 200); + processSync(mapper); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled( + AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_DOWN)))); + ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested()); + + processMove(mapper, 101, 201); + processSync(mapper); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled( + AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_MOVE)))); + processUp(mapper); + processSync(mapper); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled( + AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_UP)))); + + ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested()); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled()); +} + +TEST_F(ExternalStylusFusionTest, FusionIsRepeatedForEachNewGesture) { + SingleTouchInputMapper& mapper = initializeInputMapperWithExternalStylus(); + + ASSERT_NO_FATAL_FAILURE(testSuccessfulFusionGesture(mapper)); + ASSERT_NO_FATAL_FAILURE(testUnsuccessfulFusionGesture(mapper)); + + ASSERT_NO_FATAL_FAILURE(testSuccessfulFusionGesture(mapper)); + ASSERT_NO_FATAL_FAILURE(testSuccessfulFusionGesture(mapper)); + ASSERT_NO_FATAL_FAILURE(testUnsuccessfulFusionGesture(mapper)); + ASSERT_NO_FATAL_FAILURE(testUnsuccessfulFusionGesture(mapper)); +} + +TEST_F(ExternalStylusFusionTest, FusedPointerReportsPressureChanges) { + SingleTouchInputMapper& mapper = initializeInputMapperWithExternalStylus(); + auto toolTypeSource = + AllOf(WithSource(EXPECTED_SOURCE), WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS)); + + mStylusState.pressure = 0.8f; + processExternalStylusState(mapper); + processDown(mapper, 100, 200); + processSync(mapper); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled( + AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_DOWN), + WithPressure(0.8f)))); + ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested()); + + // The external stylus reports a pressure change. We wait for some time for a touch event. + mStylusState.pressure = 0.6f; + processExternalStylusState(mapper); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled()); + ASSERT_NO_FATAL_FAILURE( + mReader->getContext()->assertTimeoutWasRequested(ARBITRARY_TIME + TOUCH_DATA_TIMEOUT)); + + // If a touch is reported within the timeout, it reports the updated pressure. + processMove(mapper, 101, 201); + processSync(mapper); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled( + AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_MOVE), + WithPressure(0.6f)))); + ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested()); + + // There is another pressure change. + mStylusState.pressure = 0.5f; + processExternalStylusState(mapper); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled()); + ASSERT_NO_FATAL_FAILURE( + mReader->getContext()->assertTimeoutWasRequested(ARBITRARY_TIME + TOUCH_DATA_TIMEOUT)); + + // If a touch is not reported within the timeout, a move event is generated to report + // the new pressure. + handleTimeout(mapper, ARBITRARY_TIME + TOUCH_DATA_TIMEOUT); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled( + AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_MOVE), + WithPressure(0.5f)))); + + // If a zero pressure is reported before the touch goes up, the previous pressure value is + // repeated indefinitely. + mStylusState.pressure = 0.0f; + processExternalStylusState(mapper); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled()); + ASSERT_NO_FATAL_FAILURE( + mReader->getContext()->assertTimeoutWasRequested(ARBITRARY_TIME + TOUCH_DATA_TIMEOUT)); + processMove(mapper, 102, 202); + processSync(mapper); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled( + AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_MOVE), + WithPressure(0.5f)))); + processMove(mapper, 103, 203); + processSync(mapper); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled( + AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_MOVE), + WithPressure(0.5f)))); + + processUp(mapper); + processSync(mapper); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled( + AllOf(WithMotionAction(AMOTION_EVENT_ACTION_UP), WithSource(EXPECTED_SOURCE), + WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS)))); + + ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested()); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled()); +} + +TEST_F(ExternalStylusFusionTest, FusedPointerReportsToolTypeChanges) { + SingleTouchInputMapper& mapper = initializeInputMapperWithExternalStylus(); + auto source = WithSource(EXPECTED_SOURCE); + + mStylusState.pressure = 1.f; + mStylusState.toolType = AMOTION_EVENT_TOOL_TYPE_ERASER; + processExternalStylusState(mapper); + processDown(mapper, 100, 200); + processSync(mapper); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled( + AllOf(source, WithMotionAction(AMOTION_EVENT_ACTION_DOWN), + WithToolType(AMOTION_EVENT_TOOL_TYPE_ERASER)))); + ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested()); + + // The external stylus reports a tool change. We wait for some time for a touch event. + mStylusState.toolType = AMOTION_EVENT_TOOL_TYPE_STYLUS; + processExternalStylusState(mapper); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled()); + ASSERT_NO_FATAL_FAILURE( + mReader->getContext()->assertTimeoutWasRequested(ARBITRARY_TIME + TOUCH_DATA_TIMEOUT)); + + // If a touch is reported within the timeout, it reports the updated pressure. + processMove(mapper, 101, 201); + processSync(mapper); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled( + AllOf(source, WithMotionAction(AMOTION_EVENT_ACTION_MOVE), + WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS)))); + ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested()); + + // There is another tool type change. + mStylusState.toolType = AMOTION_EVENT_TOOL_TYPE_FINGER; + processExternalStylusState(mapper); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled()); + ASSERT_NO_FATAL_FAILURE( + mReader->getContext()->assertTimeoutWasRequested(ARBITRARY_TIME + TOUCH_DATA_TIMEOUT)); + + // If a touch is not reported within the timeout, a move event is generated to report + // the new tool type. + handleTimeout(mapper, ARBITRARY_TIME + TOUCH_DATA_TIMEOUT); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled( + AllOf(source, WithMotionAction(AMOTION_EVENT_ACTION_MOVE), + WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)))); + + processUp(mapper); + processSync(mapper); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled( + AllOf(source, WithMotionAction(AMOTION_EVENT_ACTION_UP), + WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER)))); + + ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested()); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled()); +} + +TEST_F(ExternalStylusFusionTest, FusedPointerReportsButtons) { + SingleTouchInputMapper& mapper = initializeInputMapperWithExternalStylus(); + auto toolTypeSource = + AllOf(WithSource(EXPECTED_SOURCE), WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS)); + + ASSERT_NO_FATAL_FAILURE(testStartFusedStylusGesture(mapper)); + + // The external stylus reports a button change. We wait for some time for a touch event. + mStylusState.buttons = AMOTION_EVENT_BUTTON_STYLUS_PRIMARY; + processExternalStylusState(mapper); + ASSERT_NO_FATAL_FAILURE( + mReader->getContext()->assertTimeoutWasRequested(ARBITRARY_TIME + TOUCH_DATA_TIMEOUT)); + + // If a touch is reported within the timeout, it reports the updated button state. + processMove(mapper, 101, 201); + processSync(mapper); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled( + AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_MOVE), + WithButtonState(AMOTION_EVENT_BUTTON_STYLUS_PRIMARY)))); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled( + AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_PRESS), + WithButtonState(AMOTION_EVENT_BUTTON_STYLUS_PRIMARY)))); + ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested()); + + // The button is now released. + mStylusState.buttons = 0; + processExternalStylusState(mapper); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled()); + ASSERT_NO_FATAL_FAILURE( + mReader->getContext()->assertTimeoutWasRequested(ARBITRARY_TIME + TOUCH_DATA_TIMEOUT)); + + // If a touch is not reported within the timeout, a move event is generated to report + // the new button state. + handleTimeout(mapper, ARBITRARY_TIME + TOUCH_DATA_TIMEOUT); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled( + AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_RELEASE), + WithButtonState(0)))); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled( + AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_MOVE), + WithButtonState(0)))); + + processUp(mapper); + processSync(mapper); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled( + AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_UP), WithButtonState(0)))); + + ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested()); + ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled()); +} + // --- MultiTouchInputMapperTest --- class MultiTouchInputMapperTest : public TouchInputMapperTest { diff --git a/services/inputflinger/tests/TestInputListener.cpp b/services/inputflinger/tests/TestInputListener.cpp index 5e47b8019b..a1299eedb4 100644 --- a/services/inputflinger/tests/TestInputListener.cpp +++ b/services/inputflinger/tests/TestInputListener.cpp @@ -82,9 +82,9 @@ void TestInputListener::assertNotifyMotionWasCalled( ASSERT_THAT(outEventArgs, matcher); } -void TestInputListener::assertNotifyMotionWasNotCalled() { +void TestInputListener::assertNotifyMotionWasNotCalled(std::optional<TimePoint> waitUntil) { ASSERT_NO_FATAL_FAILURE( - assertNotCalled<NotifyMotionArgs>("notifyMotion() should not be called.")); + assertNotCalled<NotifyMotionArgs>("notifyMotion() should not be called.", waitUntil)); } void TestInputListener::assertNotifySwitchWasCalled(NotifySwitchArgs* outEventArgs) { @@ -139,14 +139,16 @@ void TestInputListener::assertCalled(NotifyArgsType* outEventArgs, std::string m } template <class NotifyArgsType> -void TestInputListener::assertNotCalled(std::string message) { +void TestInputListener::assertNotCalled(std::string message, std::optional<TimePoint> waitUntil) { std::unique_lock<std::mutex> lock(mLock); base::ScopedLockAssertion assumeLocked(mLock); std::vector<NotifyArgsType>& queue = std::get<std::vector<NotifyArgsType>>(mQueues); - const bool eventReceived = - mCondition.wait_for(lock, mEventDidNotHappenTimeout, - [&queue]() REQUIRES(mLock) { return !queue.empty(); }); + const auto time = + waitUntil.value_or(std::chrono::system_clock::now() + mEventDidNotHappenTimeout); + const bool eventReceived = mCondition.wait_until(lock, time, [&queue]() REQUIRES(mLock) { + return !queue.empty(); + }); if (eventReceived) { FAIL() << "Unexpected event: " << message.c_str(); } diff --git a/services/inputflinger/tests/TestInputListener.h b/services/inputflinger/tests/TestInputListener.h index 87752e1487..c53f8e0497 100644 --- a/services/inputflinger/tests/TestInputListener.h +++ b/services/inputflinger/tests/TestInputListener.h @@ -33,6 +33,8 @@ public: std::chrono::milliseconds eventDidNotHappenTimeout = 0ms); virtual ~TestInputListener(); + using TimePoint = std::chrono::time_point<std::chrono::system_clock>; + void assertNotifyConfigurationChangedWasCalled( NotifyConfigurationChangedArgs* outEventArgs = nullptr); @@ -52,7 +54,7 @@ public: void assertNotifyMotionWasCalled(const ::testing::Matcher<NotifyMotionArgs>& matcher); - void assertNotifyMotionWasNotCalled(); + void assertNotifyMotionWasNotCalled(std::optional<TimePoint> waitUntil = {}); void assertNotifySwitchWasCalled(NotifySwitchArgs* outEventArgs = nullptr); @@ -66,7 +68,7 @@ private: void assertCalled(NotifyArgsType* outEventArgs, std::string message); template <class NotifyArgsType> - void assertNotCalled(std::string message); + void assertNotCalled(std::string message, std::optional<TimePoint> timeout = {}); template <class NotifyArgsType> void addToQueue(const NotifyArgsType* args); diff --git a/services/inputflinger/tests/TestInputListenerMatchers.h b/services/inputflinger/tests/TestInputListenerMatchers.h index 9a47e3e4e7..8721bd8fb0 100644 --- a/services/inputflinger/tests/TestInputListenerMatchers.h +++ b/services/inputflinger/tests/TestInputListenerMatchers.h @@ -55,6 +55,11 @@ MATCHER_P(WithDisplayId, displayId, "InputEvent with specified displayId") { return arg.displayId == displayId; } +MATCHER_P(WithDeviceId, deviceId, "InputEvent with specified deviceId") { + *result_listener << "expected deviceId " << deviceId << ", but got " << arg.deviceId; + return arg.deviceId == deviceId; +} + MATCHER_P(WithKeyCode, keyCode, "KeyEvent with specified key code") { *result_listener << "expected key code " << keyCode << ", but got " << arg.keyCode; return arg.keyCode == keyCode; @@ -70,8 +75,8 @@ MATCHER_P2(WithCoords, x, y, "InputEvent with specified coords") { MATCHER_P(WithPressure, pressure, "InputEvent with specified pressure") { const auto argPressure = arg.pointerCoords[0].getAxisValue(AMOTION_EVENT_AXIS_PRESSURE); - *result_listener << "expected pressure " << pressure << ", but got " << pressure; - return argPressure; + *result_listener << "expected pressure " << pressure << ", but got " << argPressure; + return argPressure == pressure; } MATCHER_P(WithToolType, toolType, "InputEvent with specified tool type") { diff --git a/services/inputflinger/tests/UinputDevice.cpp b/services/inputflinger/tests/UinputDevice.cpp index bc695b8bd8..97a26141e0 100644 --- a/services/inputflinger/tests/UinputDevice.cpp +++ b/services/inputflinger/tests/UinputDevice.cpp @@ -138,14 +138,36 @@ UinputSteamController::UinputSteamController() UinputExternalStylus::UinputExternalStylus() : UinputKeyboard(DEVICE_NAME, PRODUCT_ID, {BTN_STYLUS, BTN_STYLUS2, BTN_STYLUS3}) {} +// --- UinputExternalStylusWithPressure --- + +UinputExternalStylusWithPressure::UinputExternalStylusWithPressure() + : UinputKeyboard(DEVICE_NAME, PRODUCT_ID, {BTN_STYLUS, BTN_STYLUS2, BTN_STYLUS3}) {} + +void UinputExternalStylusWithPressure::configureDevice(int fd, uinput_user_dev* device) { + UinputKeyboard::configureDevice(fd, device); + + ioctl(fd, UI_SET_EVBIT, EV_ABS); + ioctl(fd, UI_SET_ABSBIT, ABS_PRESSURE); + device->absmin[ABS_PRESSURE] = RAW_PRESSURE_MIN; + device->absmax[ABS_PRESSURE] = RAW_PRESSURE_MAX; +} + +void UinputExternalStylusWithPressure::setPressure(int32_t pressure) { + injectEvent(EV_ABS, ABS_PRESSURE, pressure); + injectEvent(EV_SYN, SYN_REPORT, 0); +} + // --- UinputTouchScreen --- UinputTouchScreen::UinputTouchScreen(const Rect& size) - : UinputDevice(DEVICE_NAME, PRODUCT_ID), mSize(size) {} + : UinputKeyboard(DEVICE_NAME, PRODUCT_ID, + {BTN_TOUCH, BTN_TOOL_PEN, BTN_STYLUS, BTN_STYLUS2, BTN_STYLUS3}), + mSize(size) {} void UinputTouchScreen::configureDevice(int fd, uinput_user_dev* device) { + UinputKeyboard::configureDevice(fd, device); + // Setup the touch screen device - ioctl(fd, UI_SET_EVBIT, EV_KEY); ioctl(fd, UI_SET_EVBIT, EV_REL); ioctl(fd, UI_SET_EVBIT, EV_ABS); ioctl(fd, UI_SET_ABSBIT, ABS_MT_SLOT); @@ -155,10 +177,6 @@ void UinputTouchScreen::configureDevice(int fd, uinput_user_dev* device) { ioctl(fd, UI_SET_ABSBIT, ABS_MT_TRACKING_ID); ioctl(fd, UI_SET_ABSBIT, ABS_MT_TOOL_TYPE); ioctl(fd, UI_SET_PROPBIT, INPUT_PROP_DIRECT); - ioctl(fd, UI_SET_KEYBIT, BTN_TOUCH); - ioctl(fd, UI_SET_KEYBIT, BTN_STYLUS); - ioctl(fd, UI_SET_KEYBIT, BTN_STYLUS2); - ioctl(fd, UI_SET_KEYBIT, BTN_STYLUS3); device->absmin[ABS_MT_SLOT] = RAW_SLOT_MIN; device->absmax[ABS_MT_SLOT] = RAW_SLOT_MAX; @@ -210,10 +228,6 @@ void UinputTouchScreen::sendSync() { injectEvent(EV_SYN, SYN_REPORT, 0); } -void UinputTouchScreen::sendKey(int32_t scanCode, int32_t value) { - injectEvent(EV_KEY, scanCode, value); -} - // Get the center x, y base on the range definition. const Point UinputTouchScreen::getCenterPoint() { return Point(mSize.left + mSize.width() / 2, mSize.top + mSize.height() / 2); diff --git a/services/inputflinger/tests/UinputDevice.h b/services/inputflinger/tests/UinputDevice.h index d661bd36d3..51e331df8f 100644 --- a/services/inputflinger/tests/UinputDevice.h +++ b/services/inputflinger/tests/UinputDevice.h @@ -89,9 +89,9 @@ protected: explicit UinputKeyboard(const char* name, int16_t productId = PRODUCT_ID, std::initializer_list<int> keys = {}); -private: void configureDevice(int fd, uinput_user_dev* device) override; +private: std::set<int> mKeys; }; @@ -143,13 +143,35 @@ private: explicit UinputExternalStylus(); }; +// --- UinputExternalStylusWithPressure --- + +// A stylus that reports button presses and pressure values. +class UinputExternalStylusWithPressure : public UinputKeyboard { +public: + static constexpr const char* DEVICE_NAME = "Test Uinput External Stylus With Pressure"; + static constexpr int16_t PRODUCT_ID = 46; + + static constexpr int32_t RAW_PRESSURE_MIN = 0; + static constexpr int32_t RAW_PRESSURE_MAX = 255; + + void setPressure(int32_t pressure); + + template <class D, class... Ts> + friend std::unique_ptr<D> createUinputDevice(Ts... args); + +private: + void configureDevice(int fd, uinput_user_dev* device) override; + + explicit UinputExternalStylusWithPressure(); +}; + // --- UinputTouchScreen --- // A multi-touch touchscreen device with specific size that also supports styluses. -class UinputTouchScreen : public UinputDevice { +class UinputTouchScreen : public UinputKeyboard { public: static constexpr const char* DEVICE_NAME = "Test Uinput Touch Screen"; - static constexpr int16_t PRODUCT_ID = 46; + static constexpr int16_t PRODUCT_ID = 47; static const int32_t RAW_TOUCH_MIN = 0; static const int32_t RAW_TOUCH_MAX = 31; @@ -171,7 +193,6 @@ public: void sendUp(); void sendToolType(int32_t toolType); void sendSync(); - void sendKey(int32_t scanCode, int32_t value); const Point getCenterPoint(); |