Add timestamp smoothening for Bluetooth mice and touchpads
While Bluetooth input devices are expected to produce input events at a
consistent rate, the device may batch together several input events and
send them all at once to Android to avoid excessive Bluetooth
communication. When doing so, if the event timestamps are not processed
correctly by the kernel or the device driver, these batched events will
reach inputflinger with very similar timestamps even though they were
generated at a fixed interval.
To prevent negative user experiences with this type of Bluetooth
batching, we enforce an assumption that all Bluetooth devices generate
events at a maximum rate of 250Hz, which means each successive input
event from a Bluetooth device must have a timestamp that is at least 4
milliseconds later than the preceeding event.
Bug: 257124950
Test: atest inputflinger_tests
Test: manual, with Wacom Intuos S
Test: manual, with Apple Magic Trackpad 2
Test: manual, with Logitech MX Master 3
Test: manual, with Sony DualSense Contoller
Change-Id: Ia32bb594d0897e69796bb39f59fcc067cf487ff2
diff --git a/services/inputflinger/reader/mapper/CursorInputMapper.cpp b/services/inputflinger/reader/mapper/CursorInputMapper.cpp
index c691ca9..a4f257c 100644
--- a/services/inputflinger/reader/mapper/CursorInputMapper.cpp
+++ b/services/inputflinger/reader/mapper/CursorInputMapper.cpp
@@ -67,7 +67,7 @@
// --- CursorInputMapper ---
CursorInputMapper::CursorInputMapper(InputDeviceContext& deviceContext)
- : InputMapper(deviceContext) {}
+ : InputMapper(deviceContext), mLastEventTime(std::numeric_limits<nsecs_t>::min()) {}
CursorInputMapper::~CursorInputMapper() {
if (mPointerController != nullptr) {
@@ -276,6 +276,7 @@
std::list<NotifyArgs> CursorInputMapper::reset(nsecs_t when) {
mButtonState = 0;
mDownTime = 0;
+ mLastEventTime = std::numeric_limits<nsecs_t>::min();
mPointerVelocityControl.reset();
mWheelXVelocityControl.reset();
@@ -295,7 +296,11 @@
mCursorScrollAccumulator.process(rawEvent);
if (rawEvent->type == EV_SYN && rawEvent->code == SYN_REPORT) {
- out += sync(rawEvent->when, rawEvent->readTime);
+ const nsecs_t eventTime =
+ applyBluetoothTimestampSmoothening(getDeviceContext().getDeviceIdentifier(),
+ rawEvent->when, mLastEventTime);
+ out += sync(eventTime, rawEvent->readTime);
+ mLastEventTime = eventTime;
}
return out;
}
diff --git a/services/inputflinger/reader/mapper/CursorInputMapper.h b/services/inputflinger/reader/mapper/CursorInputMapper.h
index 6a4275e..20746e5 100644
--- a/services/inputflinger/reader/mapper/CursorInputMapper.h
+++ b/services/inputflinger/reader/mapper/CursorInputMapper.h
@@ -121,6 +121,7 @@
int32_t mButtonState;
nsecs_t mDownTime;
+ nsecs_t mLastEventTime;
void configureParameters();
void dumpParameters(std::string& dump);
diff --git a/services/inputflinger/reader/mapper/TouchCursorInputMapperCommon.h b/services/inputflinger/reader/mapper/TouchCursorInputMapperCommon.h
index 5a7ba9a..0b7ff84 100644
--- a/services/inputflinger/reader/mapper/TouchCursorInputMapperCommon.h
+++ b/services/inputflinger/reader/mapper/TouchCursorInputMapperCommon.h
@@ -101,4 +101,30 @@
return out;
}
+// For devices connected over Bluetooth, although they may produce events at a consistent rate,
+// the events might end up reaching Android in a "batched" manner through the Bluetooth
+// stack, where a few events may be clumped together and processed around the same time.
+// In this case, if the input device or its driver does not send or process the actual event
+// generation timestamps, the event time will set to whenever the kernel received the event.
+// When the timestamp deltas are minuscule for these batched events, any changes in x or y
+// coordinates result in extremely large instantaneous velocities, which can negatively impact
+// user experience. To avoid this, we augment the timestamps so that subsequent event timestamps
+// differ by at least a minimum delta value.
+static nsecs_t applyBluetoothTimestampSmoothening(const InputDeviceIdentifier& identifier,
+ nsecs_t currentEventTime, nsecs_t lastEventTime) {
+ if (identifier.bus != BUS_BLUETOOTH) {
+ return currentEventTime;
+ }
+
+ // Assume the fastest rate at which a Bluetooth touch device can report input events is one
+ // every 4 milliseconds, or 250 Hz. Timestamps for successive events from a Bluetooth device
+ // will be separated by at least this amount.
+ constexpr static nsecs_t MIN_BLUETOOTH_TIMESTAMP_DELTA = ms2ns(4);
+ // We define a maximum smoothing time delta so that we don't generate events too far into the
+ // future.
+ constexpr static nsecs_t MAX_BLUETOOTH_SMOOTHING_DELTA = ms2ns(32);
+ return std::min(std::max(currentEventTime, lastEventTime + MIN_BLUETOOTH_TIMESTAMP_DELTA),
+ currentEventTime + MAX_BLUETOOTH_SMOOTHING_DELTA);
+}
+
} // namespace android
diff --git a/services/inputflinger/reader/mapper/TouchInputMapper.cpp b/services/inputflinger/reader/mapper/TouchInputMapper.cpp
index ba3d980..605b8f8 100644
--- a/services/inputflinger/reader/mapper/TouchInputMapper.cpp
+++ b/services/inputflinger/reader/mapper/TouchInputMapper.cpp
@@ -1461,6 +1461,9 @@
const RawState& last =
mRawStatesPending.size() == 1 ? mCurrentRawState : mRawStatesPending.rbegin()[1];
+ next.when = applyBluetoothTimestampSmoothening(getDeviceContext().getDeviceIdentifier(), when,
+ last.when);
+
// Assign pointer ids.
if (!mHavePointerIds) {
assignPointerIds(last, next);
diff --git a/services/inputflinger/reader/mapper/TouchInputMapper.h b/services/inputflinger/reader/mapper/TouchInputMapper.h
index 2bb9ece..68cdef0 100644
--- a/services/inputflinger/reader/mapper/TouchInputMapper.h
+++ b/services/inputflinger/reader/mapper/TouchInputMapper.h
@@ -315,7 +315,7 @@
RawPointerAxes mRawPointerAxes;
struct RawState {
- nsecs_t when{};
+ nsecs_t when{std::numeric_limits<nsecs_t>::min()};
nsecs_t readTime{};
// Raw pointer sample data.
diff --git a/services/inputflinger/tests/InputReader_test.cpp b/services/inputflinger/tests/InputReader_test.cpp
index 0c2742f..6a174d0 100644
--- a/services/inputflinger/tests/InputReader_test.cpp
+++ b/services/inputflinger/tests/InputReader_test.cpp
@@ -99,6 +99,11 @@
// Error tolerance for floating point assertions.
static const float EPSILON = 0.001f;
+// Minimum timestamp separation between subsequent input events from a Bluetooth device.
+static constexpr nsecs_t MIN_BLUETOOTH_TIMESTAMP_DELTA = ms2ns(4);
+// Maximum smoothing time delta so that we don't generate events too far into the future.
+constexpr static nsecs_t MAX_BLUETOOTH_SMOOTHING_DELTA = ms2ns(32);
+
template<typename T>
static inline T min(T a, T b) {
return a < b ? a : b;
@@ -530,10 +535,11 @@
FakeEventHub() { }
- void addDevice(int32_t deviceId, const std::string& name,
- ftl::Flags<InputDeviceClass> classes) {
+ void addDevice(int32_t deviceId, const std::string& name, ftl::Flags<InputDeviceClass> classes,
+ int bus = 0) {
Device* device = new Device(classes);
device->identifier.name = name;
+ device->identifier.bus = bus;
mDevices.add(deviceId, device);
enqueueEvent(ARBITRARY_TIME, READ_TIME, deviceId, EventHubInterface::DEVICE_ADDED, 0, 0);
@@ -3165,13 +3171,13 @@
std::unique_ptr<InstrumentedInputReader> mReader;
std::shared_ptr<InputDevice> mDevice;
- virtual void SetUp(ftl::Flags<InputDeviceClass> classes) {
+ virtual void SetUp(ftl::Flags<InputDeviceClass> classes, int bus = 0) {
mFakeEventHub = std::make_unique<FakeEventHub>();
mFakePolicy = sp<FakeInputReaderPolicy>::make();
mFakeListener = std::make_unique<TestInputListener>();
mReader = std::make_unique<InstrumentedInputReader>(mFakeEventHub, mFakePolicy,
*mFakeListener);
- mDevice = newDevice(DEVICE_ID, DEVICE_NAME, DEVICE_LOCATION, EVENTHUB_ID, classes);
+ mDevice = newDevice(DEVICE_ID, DEVICE_NAME, DEVICE_LOCATION, EVENTHUB_ID, classes, bus);
// Consume the device reset notification generated when adding a new device.
mFakeListener->assertNotifyDeviceResetWasCalled();
}
@@ -3209,15 +3215,16 @@
std::shared_ptr<InputDevice> newDevice(int32_t deviceId, const std::string& name,
const std::string& location, int32_t eventHubId,
- ftl::Flags<InputDeviceClass> classes) {
+ ftl::Flags<InputDeviceClass> classes, int bus = 0) {
InputDeviceIdentifier identifier;
identifier.name = name;
identifier.location = location;
+ identifier.bus = bus;
std::shared_ptr<InputDevice> device =
std::make_shared<InputDevice>(mReader->getContext(), deviceId, DEVICE_GENERATION,
identifier);
mReader->pushNextDevice(device);
- mFakeEventHub->addDevice(eventHubId, name, classes);
+ mFakeEventHub->addDevice(eventHubId, name, classes, bus);
mReader->loopOnce();
return device;
}
@@ -5396,6 +5403,106 @@
ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled());
}
+// --- BluetoothCursorInputMapperTest ---
+
+class BluetoothCursorInputMapperTest : public CursorInputMapperTest {
+protected:
+ void SetUp() override {
+ InputMapperTest::SetUp(DEVICE_CLASSES | InputDeviceClass::EXTERNAL, BUS_BLUETOOTH);
+
+ mFakePointerController = std::make_shared<FakePointerController>();
+ mFakePolicy->setPointerController(mFakePointerController);
+ }
+};
+
+TEST_F(BluetoothCursorInputMapperTest, TimestampSmoothening) {
+ addConfigurationProperty("cursor.mode", "pointer");
+ CursorInputMapper& mapper = addMapperAndConfigure<CursorInputMapper>();
+
+ nsecs_t kernelEventTime = ARBITRARY_TIME;
+ nsecs_t expectedEventTime = ARBITRARY_TIME;
+ process(mapper, kernelEventTime, READ_TIME, EV_REL, REL_X, 1);
+ process(mapper, kernelEventTime, READ_TIME, EV_SYN, SYN_REPORT, 0);
+ ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+ AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE),
+ WithEventTime(expectedEventTime))));
+
+ // Process several events that come in quick succession, according to their timestamps.
+ for (int i = 0; i < 3; i++) {
+ constexpr static nsecs_t delta = ms2ns(1);
+ static_assert(delta < MIN_BLUETOOTH_TIMESTAMP_DELTA);
+ kernelEventTime += delta;
+ expectedEventTime += MIN_BLUETOOTH_TIMESTAMP_DELTA;
+
+ process(mapper, kernelEventTime, READ_TIME, EV_REL, REL_X, 1);
+ process(mapper, kernelEventTime, READ_TIME, EV_SYN, SYN_REPORT, 0);
+ ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+ AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE),
+ WithEventTime(expectedEventTime))));
+ }
+}
+
+TEST_F(BluetoothCursorInputMapperTest, TimestampSmootheningIsCapped) {
+ addConfigurationProperty("cursor.mode", "pointer");
+ CursorInputMapper& mapper = addMapperAndConfigure<CursorInputMapper>();
+
+ nsecs_t expectedEventTime = ARBITRARY_TIME;
+ process(mapper, ARBITRARY_TIME, READ_TIME, EV_REL, REL_X, 1);
+ process(mapper, ARBITRARY_TIME, READ_TIME, EV_SYN, SYN_REPORT, 0);
+ ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+ AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE),
+ WithEventTime(expectedEventTime))));
+
+ // Process several events with the same timestamp from the kernel.
+ // Ensure that we do not generate events too far into the future.
+ constexpr static int32_t numEvents =
+ MAX_BLUETOOTH_SMOOTHING_DELTA / MIN_BLUETOOTH_TIMESTAMP_DELTA;
+ for (int i = 0; i < numEvents; i++) {
+ expectedEventTime += MIN_BLUETOOTH_TIMESTAMP_DELTA;
+
+ process(mapper, ARBITRARY_TIME, READ_TIME, EV_REL, REL_X, 1);
+ process(mapper, ARBITRARY_TIME, READ_TIME, EV_SYN, SYN_REPORT, 0);
+ ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+ AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE),
+ WithEventTime(expectedEventTime))));
+ }
+
+ // By processing more events with the same timestamp, we should not generate events with a
+ // timestamp that is more than the specified max time delta from the timestamp at its injection.
+ const nsecs_t cappedEventTime = ARBITRARY_TIME + MAX_BLUETOOTH_SMOOTHING_DELTA;
+ for (int i = 0; i < 3; i++) {
+ process(mapper, ARBITRARY_TIME, READ_TIME, EV_REL, REL_X, 1);
+ process(mapper, ARBITRARY_TIME, READ_TIME, EV_SYN, SYN_REPORT, 0);
+ ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+ AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE),
+ WithEventTime(cappedEventTime))));
+ }
+}
+
+TEST_F(BluetoothCursorInputMapperTest, TimestampSmootheningNotUsed) {
+ addConfigurationProperty("cursor.mode", "pointer");
+ CursorInputMapper& mapper = addMapperAndConfigure<CursorInputMapper>();
+
+ nsecs_t kernelEventTime = ARBITRARY_TIME;
+ nsecs_t expectedEventTime = ARBITRARY_TIME;
+ process(mapper, kernelEventTime, READ_TIME, EV_REL, REL_X, 1);
+ process(mapper, kernelEventTime, READ_TIME, EV_SYN, SYN_REPORT, 0);
+ ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+ AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE),
+ WithEventTime(expectedEventTime))));
+
+ // If the next event has a timestamp that is sufficiently spaced out so that Bluetooth timestamp
+ // smoothening is not needed, its timestamp is not affected.
+ kernelEventTime += MAX_BLUETOOTH_SMOOTHING_DELTA + ms2ns(1);
+ expectedEventTime = kernelEventTime;
+
+ process(mapper, kernelEventTime, READ_TIME, EV_REL, REL_X, 1);
+ process(mapper, kernelEventTime, READ_TIME, EV_SYN, SYN_REPORT, 0);
+ ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+ AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE),
+ WithEventTime(expectedEventTime))));
+}
+
// --- TouchInputMapperTest ---
class TouchInputMapperTest : public InputMapperTest {
@@ -7389,7 +7496,8 @@
void processKey(MultiTouchInputMapper& mapper, int32_t code, int32_t value);
void processHidUsage(MultiTouchInputMapper& mapper, int32_t usageCode, int32_t value);
void processMTSync(MultiTouchInputMapper& mapper);
- void processSync(MultiTouchInputMapper& mapper);
+ void processSync(MultiTouchInputMapper& mapper, nsecs_t eventTime = ARBITRARY_TIME,
+ nsecs_t readTime = READ_TIME);
};
void MultiTouchInputMapperTest::prepareAxes(int axes) {
@@ -7502,8 +7610,9 @@
process(mapper, ARBITRARY_TIME, READ_TIME, EV_SYN, SYN_MT_REPORT, 0);
}
-void MultiTouchInputMapperTest::processSync(MultiTouchInputMapper& mapper) {
- process(mapper, ARBITRARY_TIME, READ_TIME, EV_SYN, SYN_REPORT, 0);
+void MultiTouchInputMapperTest::processSync(MultiTouchInputMapper& mapper, nsecs_t eventTime,
+ nsecs_t readTime) {
+ process(mapper, eventTime, readTime, EV_SYN, SYN_REPORT, 0);
}
TEST_F(MultiTouchInputMapperTest, Process_NormalMultiTouchGesture_WithoutTrackingIds) {
@@ -10114,6 +10223,56 @@
ASSERT_EQ(AINPUT_SOURCE_TOUCHPAD, mapper.getSources());
}
+// --- BluetoothMultiTouchInputMapperTest ---
+
+class BluetoothMultiTouchInputMapperTest : public MultiTouchInputMapperTest {
+protected:
+ void SetUp() override {
+ InputMapperTest::SetUp(DEVICE_CLASSES | InputDeviceClass::EXTERNAL, BUS_BLUETOOTH);
+ }
+};
+
+TEST_F(BluetoothMultiTouchInputMapperTest, TimestampSmoothening) {
+ addConfigurationProperty("touch.deviceType", "touchScreen");
+ prepareDisplay(DISPLAY_ORIENTATION_0);
+ prepareAxes(POSITION | ID | SLOT | PRESSURE);
+ MultiTouchInputMapper& mapper = addMapperAndConfigure<MultiTouchInputMapper>();
+
+ nsecs_t kernelEventTime = ARBITRARY_TIME;
+ nsecs_t expectedEventTime = ARBITRARY_TIME;
+ // Touch down.
+ processId(mapper, FIRST_TRACKING_ID);
+ processPosition(mapper, 100, 200);
+ processPressure(mapper, RAW_PRESSURE_MAX);
+ processSync(mapper, ARBITRARY_TIME);
+ ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+ AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), WithEventTime(ARBITRARY_TIME))));
+
+ // Process several events that come in quick succession, according to their timestamps.
+ for (int i = 0; i < 3; i++) {
+ constexpr static nsecs_t delta = ms2ns(1);
+ static_assert(delta < MIN_BLUETOOTH_TIMESTAMP_DELTA);
+ kernelEventTime += delta;
+ expectedEventTime += MIN_BLUETOOTH_TIMESTAMP_DELTA;
+
+ processPosition(mapper, 101 + i, 201 + i);
+ processSync(mapper, kernelEventTime);
+ ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+ AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE),
+ WithEventTime(expectedEventTime))));
+ }
+
+ // Release the touch.
+ processId(mapper, INVALID_TRACKING_ID);
+ processPressure(mapper, RAW_PRESSURE_MIN);
+ processSync(mapper, ARBITRARY_TIME + ms2ns(50));
+ ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+ AllOf(WithMotionAction(AMOTION_EVENT_ACTION_UP),
+ WithEventTime(ARBITRARY_TIME + ms2ns(50)))));
+}
+
+// --- MultiTouchPointerModeTest ---
+
class MultiTouchPointerModeTest : public MultiTouchInputMapperTest {
protected:
float mPointerMovementScale;
diff --git a/services/inputflinger/tests/TestInputListenerMatchers.h b/services/inputflinger/tests/TestInputListenerMatchers.h
index 5107af7..9a47e3e 100644
--- a/services/inputflinger/tests/TestInputListenerMatchers.h
+++ b/services/inputflinger/tests/TestInputListenerMatchers.h
@@ -91,4 +91,9 @@
return arg.buttonState == buttons;
}
+MATCHER_P(WithEventTime, eventTime, "InputEvent with specified eventTime") {
+ *result_listener << "expected event time " << eventTime << ", but got " << arg.eventTime;
+ return arg.eventTime == eventTime;
+}
+
} // namespace android