InputReader: Clear the multi-touch state when the device is reset
This is a partial revert of Change-Id:
Ic59d9fcb4e13fe262c422825c2485185004b719b
That CL introduced a behavior where the multi-touch state was never
reset, assuming there was no way to forcefully sync the state with the
kernel. This leads to issues when there are overflows of the kernel
buffer, in which case events may be dropped. Dropped events can lead to
the touch state being out of sync, which can result in behaviors such as
stuck pointers.
To avoid this, we will start resetting the touch state again when the
device is reset, such as after a buffer overflow.
We will follow this with the long-term fix of using EVIOCGMTSLOTS to
sync the multi-touch state in b/291626046.
Bug: 301332406
Test: atest inputflinger_tests
Test: manual, see b/301332406#comment24
Change-Id: If3027840bfff52adce086b853adc7284adf3777d
diff --git a/services/inputflinger/reader/mapper/MultiTouchInputMapper.cpp b/services/inputflinger/reader/mapper/MultiTouchInputMapper.cpp
index 2dd05f5..5a74a42 100644
--- a/services/inputflinger/reader/mapper/MultiTouchInputMapper.cpp
+++ b/services/inputflinger/reader/mapper/MultiTouchInputMapper.cpp
@@ -35,12 +35,10 @@
MultiTouchInputMapper::~MultiTouchInputMapper() {}
std::list<NotifyArgs> MultiTouchInputMapper::reset(nsecs_t when) {
- // The evdev multi-touch protocol does not allow userspace applications to query the initial or
- // current state of the pointers at any time. This means if we clear our accumulated state when
- // resetting the input mapper, there's no way to rebuild the full initial state of the pointers.
- // We can only wait for updates to all the pointers and axes. Rather than clearing the state and
- // rebuilding the state from scratch, we work around this kernel API limitation by never
- // fully clearing any state specific to the multi-touch protocol.
+ // TODO(b/291626046): Sync the MT state with the kernel using EVIOCGMTSLOTS.
+ mMultiTouchMotionAccumulator.reset(getDeviceContext());
+ mPointerIdBits.clear();
+
return TouchInputMapper::reset(when);
}
diff --git a/services/inputflinger/reader/mapper/accumulator/MultiTouchMotionAccumulator.cpp b/services/inputflinger/reader/mapper/accumulator/MultiTouchMotionAccumulator.cpp
index b0fc903..d06514a 100644
--- a/services/inputflinger/reader/mapper/accumulator/MultiTouchMotionAccumulator.cpp
+++ b/services/inputflinger/reader/mapper/accumulator/MultiTouchMotionAccumulator.cpp
@@ -30,23 +30,29 @@
size_t slotCount, bool usingSlotsProtocol) {
mUsingSlotsProtocol = usingSlotsProtocol;
mSlots = std::vector<Slot>(slotCount);
+ reset(deviceContext);
+}
- mCurrentSlot = -1;
- if (mUsingSlotsProtocol) {
- // Query the driver for the current slot index and use it as the initial slot before we
- // start reading events from the device. It is possible that the current slot index will
- // not be the same as it was when the first event was written into the evdev buffer, which
- // means the input mapper could start out of sync with the initial state of the events in
- // the evdev buffer. In the extremely unlikely case that this happens, the data from two
- // slots will be confused until the next ABS_MT_SLOT event is received. This can cause the
- // touch point to "jump", but at least there will be no stuck touches.
- int32_t initialSlot;
- if (const auto status = deviceContext.getAbsoluteAxisValue(ABS_MT_SLOT, &initialSlot);
- status == OK) {
- mCurrentSlot = initialSlot;
- } else {
- ALOGD("Could not retrieve current multi-touch slot index. status=%d", status);
- }
+void MultiTouchMotionAccumulator::reset(const InputDeviceContext& deviceContext) {
+ resetSlots();
+
+ if (!mUsingSlotsProtocol) {
+ return;
+ }
+
+ // Query the driver for the current slot index and use it as the initial slot before we
+ // start reading events from the device. It is possible that the current slot index will
+ // not be the same as it was when the first event was written into the evdev buffer, which
+ // means the input mapper could start out of sync with the initial state of the events in
+ // the evdev buffer. In the extremely unlikely case that this happens, the data from two
+ // slots will be confused until the next ABS_MT_SLOT event is received. This can cause the
+ // touch point to "jump", but at least there will be no stuck touches.
+ int32_t initialSlot;
+ if (const auto status = deviceContext.getAbsoluteAxisValue(ABS_MT_SLOT, &initialSlot);
+ status == OK) {
+ mCurrentSlot = initialSlot;
+ } else {
+ ALOGD("Could not retrieve current multi-touch slot index. status=%d", status);
}
}
diff --git a/services/inputflinger/reader/mapper/accumulator/MultiTouchMotionAccumulator.h b/services/inputflinger/reader/mapper/accumulator/MultiTouchMotionAccumulator.h
index 0e3e2bb..5b55e3d 100644
--- a/services/inputflinger/reader/mapper/accumulator/MultiTouchMotionAccumulator.h
+++ b/services/inputflinger/reader/mapper/accumulator/MultiTouchMotionAccumulator.h
@@ -83,6 +83,7 @@
LOG_ALWAYS_FATAL_IF(index < 0 || index >= mSlots.size(), "Invalid index: %zu", index);
return mSlots[index];
}
+ void reset(const InputDeviceContext& deviceContext);
private:
int32_t mCurrentSlot;
diff --git a/services/inputflinger/tests/InputReader_test.cpp b/services/inputflinger/tests/InputReader_test.cpp
index e8b779a..e0a3e94 100644
--- a/services/inputflinger/tests/InputReader_test.cpp
+++ b/services/inputflinger/tests/InputReader_test.cpp
@@ -232,6 +232,11 @@
mResetWasCalled = false;
}
+ void assertResetWasNotCalled() {
+ std::scoped_lock lock(mLock);
+ ASSERT_FALSE(mResetWasCalled) << "Expected reset to not have been called.";
+ }
+
void assertProcessWasCalled(RawEvent* outLastEvent = nullptr) {
std::unique_lock<std::mutex> lock(mLock);
base::ScopedLockAssertion assumeLocked(mLock);
@@ -248,6 +253,11 @@
mProcessWasCalled = false;
}
+ void assertProcessWasNotCalled() {
+ std::scoped_lock lock(mLock);
+ ASSERT_FALSE(mProcessWasCalled) << "Expected process to not have been called.";
+ }
+
void setKeyCodeState(int32_t keyCode, int32_t state) {
mKeyCodeStates.replaceValueFor(keyCode, state);
}
@@ -2872,6 +2882,60 @@
ASSERT_EQ(DEVICE_BLUETOOTH_ADDRESS, *address);
}
+TEST_F(InputDeviceTest, KernelBufferOverflowResetsMappers) {
+ mFakePolicy->clearViewports();
+ FakeInputMapper& mapper =
+ mDevice->addMapper<FakeInputMapper>(EVENTHUB_ID, mFakePolicy->getReaderConfiguration(),
+ AINPUT_SOURCE_KEYBOARD);
+ std::list<NotifyArgs> unused =
+ mDevice->configure(ARBITRARY_TIME, mFakePolicy->getReaderConfiguration(),
+ /*changes=*/{});
+
+ mapper.assertConfigureWasCalled();
+ mapper.assertResetWasNotCalled();
+
+ RawEvent event{.deviceId = EVENTHUB_ID,
+ .readTime = ARBITRARY_TIME,
+ .when = ARBITRARY_TIME,
+ .type = EV_SYN,
+ .code = SYN_REPORT,
+ .value = 0};
+
+ // Events are processed normally.
+ unused = mDevice->process(&event, /*count=*/1);
+ mapper.assertProcessWasCalled();
+
+ // Simulate a kernel buffer overflow, which generates a SYN_DROPPED event.
+ // This should reset the mapper.
+ event.type = EV_SYN;
+ event.code = SYN_DROPPED;
+ event.value = 0;
+ unused = mDevice->process(&event, /*count=*/1);
+ mapper.assertProcessWasNotCalled();
+ mapper.assertResetWasCalled();
+
+ // All events until the next SYN_REPORT should be dropped.
+ event.type = EV_KEY;
+ event.code = KEY_A;
+ event.value = 1;
+ unused = mDevice->process(&event, /*count=*/1);
+ mapper.assertProcessWasNotCalled();
+
+ // We get the SYN_REPORT event now, which is not forwarded to mappers.
+ event.type = EV_SYN;
+ event.code = SYN_REPORT;
+ event.value = 0;
+ unused = mDevice->process(&event, /*count=*/1);
+ mapper.assertProcessWasNotCalled();
+
+ // The mapper receives events normally now.
+ event.type = EV_KEY;
+ event.code = KEY_B;
+ event.value = 1;
+ unused = mDevice->process(&event, /*count=*/1);
+ mapper.assertProcessWasCalled();
+}
+
// --- SwitchInputMapperTest ---
class SwitchInputMapperTest : public InputMapperTest {
@@ -10907,7 +10971,7 @@
ASSERT_EQ(uint32_t(1), motionArgs.getPointerCount());
}
-TEST_F(MultiTouchInputMapperTest, Reset_PreservesLastTouchState) {
+TEST_F(MultiTouchInputMapperTest, ResetClearsTouchState) {
addConfigurationProperty("touch.deviceType", "touchScreen");
prepareDisplay(ui::ROTATION_0);
prepareAxes(POSITION | ID | SLOT | PRESSURE);
@@ -10930,25 +10994,36 @@
ASSERT_NO_FATAL_FAILURE(
mFakeListener->assertNotifyMotionWasCalled(WithMotionAction(ACTION_POINTER_1_DOWN)));
- // Reset the mapper. When the mapper is reset, we expect the current multi-touch state to be
- // preserved. Resetting should cancel the ongoing gesture.
+ // Reset the mapper. When the mapper is reset, the touch state is also cleared.
resetMapper(mapper, ARBITRARY_TIME);
ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
WithMotionAction(AMOTION_EVENT_ACTION_CANCEL)));
- // Send a sync to simulate an empty touch frame where nothing changes. The mapper should use
- // the existing touch state to generate a down event.
+ // Move the second slot pointer, and ensure there are no events, because the touch state was
+ // cleared and no slots should be in use.
processPosition(mapper, 301, 302);
processSync(mapper);
+ ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled());
+
+ // Release both fingers.
+ processId(mapper, INVALID_TRACKING_ID);
+ processSlot(mapper, FIRST_SLOT);
+ processId(mapper, INVALID_TRACKING_ID);
+ processSync(mapper);
+ ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled());
+
+ // Start a new gesture, and ensure we get a DOWN event for it.
+ processId(mapper, FIRST_TRACKING_ID);
+ processPosition(mapper, 200, 300);
+ processPressure(mapper, RAW_PRESSURE_MAX);
+ processSync(mapper);
ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), WithPressure(1.f))));
- ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
- AllOf(WithMotionAction(ACTION_POINTER_1_DOWN), WithPressure(1.f))));
ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled());
}
-TEST_F(MultiTouchInputMapperTest, Reset_PreservesLastTouchState_NoPointersDown) {
+TEST_F(MultiTouchInputMapperTest, ResetClearsTouchStateWithNoPointersDown) {
addConfigurationProperty("touch.deviceType", "touchScreen");
prepareDisplay(ui::ROTATION_0);
prepareAxes(POSITION | ID | SLOT | PRESSURE);
@@ -11076,6 +11151,66 @@
ASSERT_FALSE(fakePointerController->isPointerShown());
}
+TEST_F(MultiTouchInputMapperTest, SimulateKernelBufferOverflow) {
+ addConfigurationProperty("touch.deviceType", "touchScreen");
+ prepareDisplay(ui::ROTATION_0);
+ prepareAxes(POSITION | ID | SLOT | PRESSURE);
+ MultiTouchInputMapper& mapper = constructAndAddMapper<MultiTouchInputMapper>();
+
+ // First finger down.
+ processId(mapper, FIRST_TRACKING_ID);
+ processPosition(mapper, 100, 200);
+ processPressure(mapper, RAW_PRESSURE_MAX);
+ processSync(mapper);
+ ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+ WithMotionAction(AMOTION_EVENT_ACTION_DOWN)));
+
+ // Assume the kernel buffer overflows, and we get a SYN_DROPPED event.
+ // This will reset the mapper, and thus also reset the touch state.
+ process(mapper, ARBITRARY_TIME, READ_TIME, EV_SYN, SYN_DROPPED, 0);
+ resetMapper(mapper, ARBITRARY_TIME);
+ ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+ WithMotionAction(AMOTION_EVENT_ACTION_CANCEL)));
+
+ // Since the touch state was reset, it doesn't know which slots are active, so any movements
+ // are ignored.
+ processPosition(mapper, 101, 201);
+ processSync(mapper);
+
+ ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled());
+
+ // Second finger goes down. This is the first active finger, so we get a DOWN event.
+ processSlot(mapper, SECOND_SLOT);
+ processId(mapper, SECOND_TRACKING_ID);
+ processPosition(mapper, 400, 500);
+ processPressure(mapper, RAW_PRESSURE_MAX);
+ processSync(mapper);
+
+ ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+ WithMotionAction(AMOTION_EVENT_ACTION_DOWN)));
+
+ // First slot is still ignored, only the second one is active.
+ processSlot(mapper, FIRST_SLOT);
+ processPosition(mapper, 102, 202);
+ processSlot(mapper, SECOND_SLOT);
+ processPosition(mapper, 401, 501);
+ processSync(mapper);
+
+ ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+ WithMotionAction(AMOTION_EVENT_ACTION_MOVE)));
+
+ // Both slots up, so we get the UP event for the active pointer.
+ processSlot(mapper, FIRST_SLOT);
+ processId(mapper, INVALID_TRACKING_ID);
+ processSlot(mapper, SECOND_SLOT);
+ processId(mapper, INVALID_TRACKING_ID);
+ processSync(mapper);
+
+ ASSERT_NO_FATAL_FAILURE(
+ mFakeListener->assertNotifyMotionWasCalled(WithMotionAction(AMOTION_EVENT_ACTION_UP)));
+ ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled());
+}
+
// --- MultiTouchInputMapperTest_ExternalDevice ---
class MultiTouchInputMapperTest_ExternalDevice : public MultiTouchInputMapperTest {