Create Input SlopController
Create a basic input slop controller, and use it for rotary inputs.
Bug: 285957835
Test: atest SlopControllerTest
Change-Id: I0cabfed1f064b849f8151f069e1241292cc374cf
diff --git a/services/inputflinger/reader/Android.bp b/services/inputflinger/reader/Android.bp
index b0edb57..a896d26 100644
--- a/services/inputflinger/reader/Android.bp
+++ b/services/inputflinger/reader/Android.bp
@@ -52,6 +52,7 @@
"mapper/RotaryEncoderInputMapper.cpp",
"mapper/SensorInputMapper.cpp",
"mapper/SingleTouchInputMapper.cpp",
+ "mapper/SlopController.cpp",
"mapper/SwitchInputMapper.cpp",
"mapper/TouchCursorInputMapperCommon.cpp",
"mapper/TouchInputMapper.cpp",
diff --git a/services/inputflinger/reader/mapper/RotaryEncoderInputMapper.cpp b/services/inputflinger/reader/mapper/RotaryEncoderInputMapper.cpp
index 13f2e59..5220b10 100644
--- a/services/inputflinger/reader/mapper/RotaryEncoderInputMapper.cpp
+++ b/services/inputflinger/reader/mapper/RotaryEncoderInputMapper.cpp
@@ -30,6 +30,14 @@
const InputReaderConfiguration& readerConfig)
: InputMapper(deviceContext, readerConfig), mOrientation(ui::ROTATION_0) {
mSource = AINPUT_SOURCE_ROTARY_ENCODER;
+
+ const PropertyMap& config = getDeviceContext().getConfiguration();
+ float slopThreshold = config.getInt("rotary_encoder.slop_threshold").value_or(0);
+ int32_t slopDurationMs = config.getInt("rotary_encoder.slop_duration_ms").value_or(0);
+ if (slopThreshold > 0 && slopDurationMs > 0) {
+ mSlopController = std::make_unique<SlopController>(slopThreshold,
+ (nsecs_t)(slopDurationMs * 1000000));
+ }
}
RotaryEncoderInputMapper::~RotaryEncoderInputMapper() {}
@@ -103,6 +111,10 @@
std::list<NotifyArgs> out;
float scroll = mRotaryEncoderScrollAccumulator.getRelativeVWheel();
+ if (mSlopController) {
+ scroll = mSlopController->consumeEvent(when, scroll);
+ }
+
bool scrolled = scroll != 0;
// Send motion event.
diff --git a/services/inputflinger/reader/mapper/RotaryEncoderInputMapper.h b/services/inputflinger/reader/mapper/RotaryEncoderInputMapper.h
index 9e2e8c4..4732bcd 100644
--- a/services/inputflinger/reader/mapper/RotaryEncoderInputMapper.h
+++ b/services/inputflinger/reader/mapper/RotaryEncoderInputMapper.h
@@ -20,6 +20,7 @@
#include "CursorScrollAccumulator.h"
#include "InputMapper.h"
+#include "SlopController.h"
namespace android {
@@ -46,6 +47,7 @@
int32_t mSource;
float mScalingFactor;
ui::Rotation mOrientation;
+ std::unique_ptr<SlopController> mSlopController = nullptr;
explicit RotaryEncoderInputMapper(InputDeviceContext& deviceContext,
const InputReaderConfiguration& readerConfig);
diff --git a/services/inputflinger/reader/mapper/SlopController.cpp b/services/inputflinger/reader/mapper/SlopController.cpp
new file mode 100644
index 0000000..6f31d0e
--- /dev/null
+++ b/services/inputflinger/reader/mapper/SlopController.cpp
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// clang-format off
+#include "../Macros.h"
+// clang-format on
+
+#include "SlopController.h"
+
+namespace {
+int signOf(float value) {
+ if (value == 0) return 0;
+ if (value > 0) return 1;
+ return -1;
+}
+} // namespace
+
+namespace android {
+
+SlopController::SlopController(float slopThreshold, nsecs_t slopDurationNanos)
+ : mSlopThreshold(slopThreshold), mSlopDurationNanos(slopDurationNanos) {}
+
+SlopController::~SlopController() {}
+
+float SlopController::consumeEvent(nsecs_t eventTimeNanos, float value) {
+ if (mSlopDurationNanos == 0) {
+ return value;
+ }
+
+ if (shouldResetSlopTracking(eventTimeNanos, value)) {
+ mCumulativeValue = 0;
+ mHasSlopBeenMet = false;
+ }
+
+ mLastEventTimeNanos = eventTimeNanos;
+
+ if (mHasSlopBeenMet) {
+ // Since slop has already been met, we know that all of the current value would pass the
+ // slop threshold. So return that, without any further processing.
+ return value;
+ }
+
+ mCumulativeValue += value;
+
+ if (abs(mCumulativeValue) >= mSlopThreshold) {
+ mHasSlopBeenMet = true;
+ // Return the amount of value that exceeds the slop.
+ return signOf(value) * (abs(mCumulativeValue) - mSlopThreshold);
+ }
+
+ return 0;
+}
+
+bool SlopController::shouldResetSlopTracking(nsecs_t eventTimeNanos, float value) {
+ const nsecs_t ageNanos = eventTimeNanos - mLastEventTimeNanos;
+ if (ageNanos >= mSlopDurationNanos) {
+ return true;
+ }
+ if (value == 0) {
+ return false;
+ }
+ if (signOf(mCumulativeValue) != signOf(value)) {
+ return true;
+ }
+ return false;
+}
+
+} // namespace android
diff --git a/services/inputflinger/reader/mapper/SlopController.h b/services/inputflinger/reader/mapper/SlopController.h
new file mode 100644
index 0000000..bd6ee77
--- /dev/null
+++ b/services/inputflinger/reader/mapper/SlopController.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <utils/Timers.h>
+
+namespace android {
+
+/**
+ * Controls a slop logic. Slop here refers to an approach to try and drop insignificant input
+ * events. This is helpful in cases where unintentional input events may cause unintended outcomes,
+ * like scrolling a screen or keeping the screen awake.
+ *
+ * Current slop logic:
+ * "If time since last event > Xns, then discard the next N values."
+ */
+class SlopController {
+public:
+ SlopController(float slopThreshold, nsecs_t slopDurationNanos);
+ virtual ~SlopController();
+
+ /**
+ * Consumes an event with a given time and value for slop processing.
+ * Returns an amount <=value that should be consumed.
+ */
+ float consumeEvent(nsecs_t eventTime, float value);
+
+private:
+ bool shouldResetSlopTracking(nsecs_t eventTimeNanos, float value);
+
+ /** The amount of event values ignored after an inactivity of the slop duration. */
+ const float mSlopThreshold;
+ /** The duration of inactivity that resets slop controlling. */
+ const nsecs_t mSlopDurationNanos;
+
+ nsecs_t mLastEventTimeNanos = 0;
+ float mCumulativeValue = 0;
+ bool mHasSlopBeenMet = false;
+};
+
+} // namespace android
diff --git a/services/inputflinger/tests/Android.bp b/services/inputflinger/tests/Android.bp
index 300bb85..370e971 100644
--- a/services/inputflinger/tests/Android.bp
+++ b/services/inputflinger/tests/Android.bp
@@ -59,6 +59,7 @@
"NotifyArgs_test.cpp",
"PreferStylusOverTouch_test.cpp",
"PropertyProvider_test.cpp",
+ "SlopController_test.cpp",
"SyncQueue_test.cpp",
"TestInputListener.cpp",
"TouchpadInputMapper_test.cpp",
diff --git a/services/inputflinger/tests/SlopController_test.cpp b/services/inputflinger/tests/SlopController_test.cpp
new file mode 100644
index 0000000..f524acd
--- /dev/null
+++ b/services/inputflinger/tests/SlopController_test.cpp
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "../reader/mapper/SlopController.h"
+
+#include <gtest/gtest.h>
+
+namespace android {
+
+// --- SlopControllerTest ---
+
+TEST(SlopControllerTest, PositiveValues) {
+ SlopController controller = SlopController(/*slopThreshold=*/5, /*slopDurationNanos=*/100);
+
+ ASSERT_EQ(0, controller.consumeEvent(1000, 1));
+ ASSERT_EQ(0, controller.consumeEvent(1003, 3));
+ ASSERT_EQ(2, controller.consumeEvent(1005, 3));
+ ASSERT_EQ(4, controller.consumeEvent(1009, 4));
+
+ SlopController controller2 = SlopController(/*slopThreshold=*/5, /*slopDurationNanos=*/100);
+
+ ASSERT_EQ(0, controller2.consumeEvent(1000, 5));
+ ASSERT_EQ(3, controller2.consumeEvent(1003, 3));
+ ASSERT_EQ(4, controller2.consumeEvent(1005, 4));
+}
+
+TEST(SlopControllerTest, NegativeValues) {
+ SlopController controller = SlopController(/*slopThreshold=*/5, /*slopDurationNanos=*/100);
+
+ ASSERT_EQ(0, controller.consumeEvent(1000, -1));
+ ASSERT_EQ(0, controller.consumeEvent(1003, -3));
+ ASSERT_EQ(-2, controller.consumeEvent(1005, -3));
+ ASSERT_EQ(-4, controller.consumeEvent(1009, -4));
+
+ SlopController controller2 = SlopController(/*slopThreshold=*/5, /*slopDurationNanos=*/100);
+
+ ASSERT_EQ(0, controller2.consumeEvent(1000, -5));
+ ASSERT_EQ(-3, controller2.consumeEvent(1003, -3));
+ ASSERT_EQ(-4, controller2.consumeEvent(1005, -4));
+}
+
+TEST(SlopControllerTest, ZeroDoesNotResetSlop) {
+ SlopController controller = SlopController(/*slopThreshold=*/5, /*slopDurationNanos=*/100);
+
+ ASSERT_EQ(1, controller.consumeEvent(1005, 6));
+ ASSERT_EQ(0, controller.consumeEvent(1006, 0));
+ ASSERT_EQ(2, controller.consumeEvent(1008, 2));
+}
+
+TEST(SlopControllerTest, SignChange_ResetsSlop) {
+ SlopController controller = SlopController(/*slopThreshold=*/5, /*slopDurationNanos=*/100);
+
+ ASSERT_EQ(0, controller.consumeEvent(1000, 2));
+ ASSERT_EQ(0, controller.consumeEvent(1001, -4));
+ ASSERT_EQ(0, controller.consumeEvent(1002, 3));
+ ASSERT_EQ(0, controller.consumeEvent(1003, -2));
+
+ ASSERT_EQ(1, controller.consumeEvent(1005, 6));
+ ASSERT_EQ(0, controller.consumeEvent(1006, 0));
+ ASSERT_EQ(2, controller.consumeEvent(1008, 2));
+
+ ASSERT_EQ(0, controller.consumeEvent(1010, -4));
+ ASSERT_EQ(-1, controller.consumeEvent(1011, -2));
+
+ ASSERT_EQ(0, controller.consumeEvent(1015, 5));
+ ASSERT_EQ(2, controller.consumeEvent(1016, 2));
+
+ ASSERT_EQ(0, controller.consumeEvent(1017, -5));
+ ASSERT_EQ(-2, controller.consumeEvent(1018, -2));
+}
+
+TEST(SlopControllerTest, OldAge_ResetsSlop) {
+ SlopController controller = SlopController(/*slopThreshold=*/5, /*slopDurationNanos=*/100);
+
+ ASSERT_EQ(1, controller.consumeEvent(1005, 6));
+ ASSERT_EQ(0, controller.consumeEvent(1108, 2)); // age exceeds slop duration
+
+ ASSERT_EQ(1, controller.consumeEvent(1110, 4));
+ ASSERT_EQ(0, controller.consumeEvent(1210, 2)); // age equals slop duration
+
+ ASSERT_EQ(0, controller.consumeEvent(1215, -3));
+ ASSERT_EQ(-2, controller.consumeEvent(1216, -4));
+ ASSERT_EQ(-5, controller.consumeEvent(1315, -5));
+}
+
+} // namespace android