Merge changes Iebd90276,I645481db,I2c00fca0,I289e4470
* changes:
TouchInputMapper: s/mExternalStylusId/mFusedStylusPointerId
Ensure stylus buttons generate events consistently
Add integration tests to verify the behavior of a fused external stylus
Add unit tests for external stylus fusion
diff --git a/services/inputflinger/reader/mapper/TouchInputMapper.cpp b/services/inputflinger/reader/mapper/TouchInputMapper.cpp
index bf73ce5..8bae650 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 @@
// --- 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 @@
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 @@
void TouchInputMapper::resetExternalStylus() {
mExternalStylusState.clear();
- mExternalStylusId = -1;
+ mFusedStylusPointerId.reset();
mExternalStylusFusionTimeout = LLONG_MAX;
mExternalStylusDataPending = false;
+ mExternalStylusButtonsApplied = 0;
}
void TouchInputMapper::clearStylusDataPendingFlags() {
@@ -1693,8 +1691,17 @@
}
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 @@
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 @@
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 @@
}
// 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 @@
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::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 c20f28b..fba7b79 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 @@
// 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 879d36e..8a361fa 100644
--- a/services/inputflinger/tests/InputReader_test.cpp
+++ b/services/inputflinger/tests/InputReader_test.cpp
@@ -1349,6 +1349,8 @@
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 @@
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 @@
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 @@
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(
- AllOf(WithKeyAction(AKEY_EVENT_ACTION_DOWN), WithSource(AINPUT_SOURCE_KEYBOARD),
- WithKeyCode(AKEYCODE_STYLUS_BUTTON_PRIMARY))));
+// --- StylusButtonIntegrationTest ---
- mDevice->sendKey(BTN_STYLUS, 0);
- mDevice->sendSync();
- ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyKeyWasCalled(
+// 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), WithDeviceId(stylusId))));
+
+ 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.
+ 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),
+ WithDeviceId(touchscreenId))));
+
+ // Press and release a stylus button. Each change in button state also generates a MOVE event.
+ 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), 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),
+ 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),
+ WithDeviceId(touchscreenId))));
+
+ 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), WithDeviceId(stylusId))));
+ ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyMotionWasCalled(
+ AllOf(WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_RELEASE),
+ 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),
+ 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_PEN);
+ 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))));
+ WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0),
+ WithDeviceId(touchscreenId), WithPressure(100.f / RAW_PRESSURE_MAX))));
- // 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(
- AllOf(WithKeyAction(AKEY_EVENT_ACTION_DOWN), WithSource(AINPUT_SOURCE_KEYBOARD),
- WithKeyCode(AKEYCODE_STYLUS_BUTTON_PRIMARY))));
+ // 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(AMOTION_EVENT_BUTTON_STYLUS_PRIMARY))));
- ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled(
- AllOf(WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_PRESS),
- WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS),
- WithButtonState(AMOTION_EVENT_BUTTON_STYLUS_PRIMARY))));
+ WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0),
+ WithDeviceId(touchscreenId), WithPressure(200.f / RAW_PRESSURE_MAX))));
- mDevice->sendKey(BTN_STYLUS, 0);
+ // 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->assertNotifyKeyWasCalled(
- AllOf(WithKeyAction(AKEY_EVENT_ACTION_UP), WithSource(AINPUT_SOURCE_KEYBOARD),
- WithKeyCode(AKEYCODE_STYLUS_BUTTON_PRIMARY))));
- ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled(
- AllOf(WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_RELEASE),
- WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0))));
- ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled(
- AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE),
- WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0))));
+ ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasNotCalled(waitUntil));
- // Finish the stylus gesture.
+ // 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 @@
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 @@
}
}
+// --- 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 5e47b80..a1299ee 100644
--- a/services/inputflinger/tests/TestInputListener.cpp
+++ b/services/inputflinger/tests/TestInputListener.cpp
@@ -82,9 +82,9 @@
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 @@
}
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 87752e1..c53f8e0 100644
--- a/services/inputflinger/tests/TestInputListener.h
+++ b/services/inputflinger/tests/TestInputListener.h
@@ -33,6 +33,8 @@
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 @@
void assertNotifyMotionWasCalled(const ::testing::Matcher<NotifyMotionArgs>& matcher);
- void assertNotifyMotionWasNotCalled();
+ void assertNotifyMotionWasNotCalled(std::optional<TimePoint> waitUntil = {});
void assertNotifySwitchWasCalled(NotifySwitchArgs* outEventArgs = nullptr);
@@ -66,7 +68,7 @@
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 9a47e3e..8721bd8 100644
--- a/services/inputflinger/tests/TestInputListenerMatchers.h
+++ b/services/inputflinger/tests/TestInputListenerMatchers.h
@@ -55,6 +55,11 @@
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_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 bc695b8..97a2614 100644
--- a/services/inputflinger/tests/UinputDevice.cpp
+++ b/services/inputflinger/tests/UinputDevice.cpp
@@ -138,14 +138,36 @@
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 @@
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 @@
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 d661bd3..51e331d 100644
--- a/services/inputflinger/tests/UinputDevice.h
+++ b/services/inputflinger/tests/UinputDevice.h
@@ -89,9 +89,9 @@
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 @@
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 @@
void sendUp();
void sendToolType(int32_t toolType);
void sendSync();
- void sendKey(int32_t scanCode, int32_t value);
const Point getCenterPoint();