1 /*
2 * Copyright 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 #include "RotaryEncoderInputMapper.h"
18
19 #include <list>
20 #include <string>
21 #include <tuple>
22 #include <variant>
23
24 #include <android-base/logging.h>
25 #include <android_companion_virtualdevice_flags.h>
26 #include <com_android_input_flags.h>
27 #include <flag_macros.h>
28 #include <gtest/gtest.h>
29 #include <input/DisplayViewport.h>
30 #include <linux/input-event-codes.h>
31 #include <linux/input.h>
32 #include <utils/Timers.h>
33
34 #include "InputMapperTest.h"
35 #include "InputReaderBase.h"
36 #include "InterfaceMocks.h"
37 #include "NotifyArgs.h"
38 #include "TestEventMatchers.h"
39 #include "ui/Rotation.h"
40
41 #define TAG "RotaryEncoderInputMapper_test"
42
43 namespace android {
44
45 using testing::AllOf;
46 using testing::Return;
47 using testing::VariantWith;
48 constexpr ui::LogicalDisplayId DISPLAY_ID = ui::LogicalDisplayId::DEFAULT;
49 constexpr ui::LogicalDisplayId SECONDARY_DISPLAY_ID = ui::LogicalDisplayId{DISPLAY_ID.val() + 1};
50 constexpr int32_t DISPLAY_WIDTH = 480;
51 constexpr int32_t DISPLAY_HEIGHT = 800;
52
53 namespace {
54
createViewport()55 DisplayViewport createViewport() {
56 DisplayViewport v;
57 v.orientation = ui::Rotation::Rotation0;
58 v.logicalRight = DISPLAY_HEIGHT;
59 v.logicalBottom = DISPLAY_WIDTH;
60 v.physicalRight = DISPLAY_HEIGHT;
61 v.physicalBottom = DISPLAY_WIDTH;
62 v.deviceWidth = DISPLAY_HEIGHT;
63 v.deviceHeight = DISPLAY_WIDTH;
64 v.isActive = true;
65 return v;
66 }
67
createPrimaryViewport()68 DisplayViewport createPrimaryViewport() {
69 DisplayViewport v = createViewport();
70 v.displayId = DISPLAY_ID;
71 v.uniqueId = "local:1";
72 return v;
73 }
74
createSecondaryViewport()75 DisplayViewport createSecondaryViewport() {
76 DisplayViewport v = createViewport();
77 v.displayId = SECONDARY_DISPLAY_ID;
78 v.uniqueId = "local:2";
79 v.type = ViewportType::EXTERNAL;
80 return v;
81 }
82
83 } // namespace
84
85 namespace vd_flags = android::companion::virtualdevice::flags;
86
87 /**
88 * Unit tests for RotaryEncoderInputMapper.
89 */
90 class RotaryEncoderInputMapperTest : public InputMapperUnitTest {
91 protected:
SetUp()92 void SetUp() override { SetUpWithBus(BUS_USB); }
SetUpWithBus(int bus)93 void SetUpWithBus(int bus) override {
94 InputMapperUnitTest::SetUpWithBus(bus);
95
96 EXPECT_CALL(mMockEventHub, hasRelativeAxis(EVENTHUB_ID, REL_WHEEL))
97 .WillRepeatedly(Return(true));
98 EXPECT_CALL(mMockEventHub, hasRelativeAxis(EVENTHUB_ID, REL_HWHEEL))
99 .WillRepeatedly(Return(false));
100 EXPECT_CALL(mMockEventHub, hasRelativeAxis(EVENTHUB_ID, REL_WHEEL_HI_RES))
101 .WillRepeatedly(Return(false));
102 EXPECT_CALL(mMockEventHub, hasRelativeAxis(EVENTHUB_ID, REL_HWHEEL_HI_RES))
103 .WillRepeatedly(Return(false));
104 }
105
106 std::map<std::string, int64_t> mTelemetryLogCounts;
107
108 /**
109 * A fake function for telemetry logging.
110 * Records the log counts in the `mTelemetryLogCounts` map.
111 */
112 std::function<void(const char*, int64_t)> mTelemetryLogCounter =
__anon99f72efc0202(const char* key, int64_t value) 113 [this](const char* key, int64_t value) { mTelemetryLogCounts[key] += value; };
114 };
115
TEST_F(RotaryEncoderInputMapperTest,ConfigureDisplayIdWithAssociatedViewport)116 TEST_F(RotaryEncoderInputMapperTest, ConfigureDisplayIdWithAssociatedViewport) {
117 DisplayViewport primaryViewport = createPrimaryViewport();
118 DisplayViewport secondaryViewport = createSecondaryViewport();
119 mReaderConfiguration.setDisplayViewports({primaryViewport, secondaryViewport});
120
121 // Set up the secondary display as the associated viewport of the mapper.
122 EXPECT_CALL((*mDevice), getAssociatedViewport).WillRepeatedly(Return(secondaryViewport));
123 mMapper = createInputMapper<RotaryEncoderInputMapper>(*mDeviceContext, mReaderConfiguration);
124
125 std::list<NotifyArgs> args;
126 // Ensure input events are generated for the secondary display.
127 args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, 1);
128 args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);
129 EXPECT_THAT(args,
130 ElementsAre(VariantWith<NotifyMotionArgs>(
131 AllOf(WithMotionAction(AMOTION_EVENT_ACTION_SCROLL),
132 WithSource(AINPUT_SOURCE_ROTARY_ENCODER),
133 WithDisplayId(SECONDARY_DISPLAY_ID)))));
134 }
135
TEST_F(RotaryEncoderInputMapperTest,ConfigureDisplayIdNoAssociatedViewport)136 TEST_F(RotaryEncoderInputMapperTest, ConfigureDisplayIdNoAssociatedViewport) {
137 // Set up the default display.
138 mFakePolicy->clearViewports();
139 mFakePolicy->addDisplayViewport(createPrimaryViewport());
140
141 // Set up the mapper with no associated viewport.
142 mMapper = createInputMapper<RotaryEncoderInputMapper>(*mDeviceContext, mReaderConfiguration);
143
144 // Ensure input events are generated without display ID
145 std::list<NotifyArgs> args;
146 args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, 1);
147 args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);
148 EXPECT_THAT(args,
149 ElementsAre(VariantWith<NotifyMotionArgs>(
150 AllOf(WithMotionAction(AMOTION_EVENT_ACTION_SCROLL),
151 WithSource(AINPUT_SOURCE_ROTARY_ENCODER),
152 WithDisplayId(ui::LogicalDisplayId::INVALID)))));
153 }
154
TEST_F(RotaryEncoderInputMapperTest,ProcessRegularScroll)155 TEST_F(RotaryEncoderInputMapperTest, ProcessRegularScroll) {
156 mMapper = createInputMapper<RotaryEncoderInputMapper>(*mDeviceContext, mReaderConfiguration);
157
158 std::list<NotifyArgs> args;
159 args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, 1);
160 args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);
161
162 EXPECT_THAT(args,
163 ElementsAre(VariantWith<NotifyMotionArgs>(
164 AllOf(WithSource(AINPUT_SOURCE_ROTARY_ENCODER),
165 WithMotionAction(AMOTION_EVENT_ACTION_SCROLL), WithScroll(1.0f)))));
166 }
167
TEST_F(RotaryEncoderInputMapperTest,ProcessHighResScroll)168 TEST_F(RotaryEncoderInputMapperTest, ProcessHighResScroll) {
169 vd_flags::high_resolution_scroll(true);
170 EXPECT_CALL(mMockEventHub, hasRelativeAxis(EVENTHUB_ID, REL_WHEEL_HI_RES))
171 .WillRepeatedly(Return(true));
172 mMapper = createInputMapper<RotaryEncoderInputMapper>(*mDeviceContext, mReaderConfiguration);
173
174 std::list<NotifyArgs> args;
175 args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL_HI_RES, 60);
176 args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);
177
178 EXPECT_THAT(args,
179 ElementsAre(VariantWith<NotifyMotionArgs>(
180 AllOf(WithSource(AINPUT_SOURCE_ROTARY_ENCODER),
181 WithMotionAction(AMOTION_EVENT_ACTION_SCROLL), WithScroll(0.5f)))));
182 }
183
TEST_F(RotaryEncoderInputMapperTest,HighResScrollIgnoresRegularScroll)184 TEST_F(RotaryEncoderInputMapperTest, HighResScrollIgnoresRegularScroll) {
185 vd_flags::high_resolution_scroll(true);
186 EXPECT_CALL(mMockEventHub, hasRelativeAxis(EVENTHUB_ID, REL_WHEEL_HI_RES))
187 .WillRepeatedly(Return(true));
188 mMapper = createInputMapper<RotaryEncoderInputMapper>(*mDeviceContext, mReaderConfiguration);
189
190 std::list<NotifyArgs> args;
191 args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL_HI_RES, 60);
192 args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, 1);
193 args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);
194
195 EXPECT_THAT(args,
196 ElementsAre(VariantWith<NotifyMotionArgs>(
197 AllOf(WithSource(AINPUT_SOURCE_ROTARY_ENCODER),
198 WithMotionAction(AMOTION_EVENT_ACTION_SCROLL), WithScroll(0.5f)))));
199 }
200
TEST_F_WITH_FLAGS(RotaryEncoderInputMapperTest,RotaryInputTelemetryFlagOff_NoRotationLogging,REQUIRES_FLAGS_DISABLED (ACONFIG_FLAG (com::android::input::flags,rotary_input_telemetry)))201 TEST_F_WITH_FLAGS(RotaryEncoderInputMapperTest, RotaryInputTelemetryFlagOff_NoRotationLogging,
202 REQUIRES_FLAGS_DISABLED(ACONFIG_FLAG(com::android::input::flags,
203 rotary_input_telemetry))) {
204 mPropertyMap.addProperty("device.res", "3");
205 mMapper = createInputMapper<RotaryEncoderInputMapper>(*mDeviceContext, mReaderConfiguration,
206 mTelemetryLogCounter);
207 InputDeviceInfo info;
208 mMapper->populateDeviceInfo(info);
209
210 std::list<NotifyArgs> args;
211 args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, 70);
212 args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);
213
214 ASSERT_EQ(mTelemetryLogCounts.find("input.value_rotary_input_device_full_rotation_count"),
215 mTelemetryLogCounts.end());
216 }
217
TEST_F_WITH_FLAGS(RotaryEncoderInputMapperTest,ZeroResolution_NoRotationLogging,REQUIRES_FLAGS_ENABLED (ACONFIG_FLAG (com::android::input::flags,rotary_input_telemetry)))218 TEST_F_WITH_FLAGS(RotaryEncoderInputMapperTest, ZeroResolution_NoRotationLogging,
219 REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(com::android::input::flags,
220 rotary_input_telemetry))) {
221 mPropertyMap.addProperty("device.res", "-3");
222 mPropertyMap.addProperty("rotary_encoder.min_rotations_to_log", "2");
223 mMapper = createInputMapper<RotaryEncoderInputMapper>(*mDeviceContext, mReaderConfiguration,
224 mTelemetryLogCounter);
225 InputDeviceInfo info;
226 mMapper->populateDeviceInfo(info);
227
228 std::list<NotifyArgs> args;
229 args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, 700);
230 args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);
231
232 ASSERT_EQ(mTelemetryLogCounts.find("input.value_rotary_input_device_full_rotation_count"),
233 mTelemetryLogCounts.end());
234 }
235
TEST_F_WITH_FLAGS(RotaryEncoderInputMapperTest,NegativeMinLogRotation_NoRotationLogging,REQUIRES_FLAGS_ENABLED (ACONFIG_FLAG (com::android::input::flags,rotary_input_telemetry)))236 TEST_F_WITH_FLAGS(RotaryEncoderInputMapperTest, NegativeMinLogRotation_NoRotationLogging,
237 REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(com::android::input::flags,
238 rotary_input_telemetry))) {
239 mPropertyMap.addProperty("device.res", "3");
240 mPropertyMap.addProperty("rotary_encoder.min_rotations_to_log", "-2");
241 mMapper = createInputMapper<RotaryEncoderInputMapper>(*mDeviceContext, mReaderConfiguration,
242 mTelemetryLogCounter);
243 InputDeviceInfo info;
244 mMapper->populateDeviceInfo(info);
245
246 std::list<NotifyArgs> args;
247 args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, 700);
248 args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);
249
250 ASSERT_EQ(mTelemetryLogCounts.find("input.value_rotary_input_device_full_rotation_count"),
251 mTelemetryLogCounts.end());
252 }
253
TEST_F_WITH_FLAGS(RotaryEncoderInputMapperTest,ZeroMinLogRotation_NoRotationLogging,REQUIRES_FLAGS_ENABLED (ACONFIG_FLAG (com::android::input::flags,rotary_input_telemetry)))254 TEST_F_WITH_FLAGS(RotaryEncoderInputMapperTest, ZeroMinLogRotation_NoRotationLogging,
255 REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(com::android::input::flags,
256 rotary_input_telemetry))) {
257 mPropertyMap.addProperty("device.res", "3");
258 mPropertyMap.addProperty("rotary_encoder.min_rotations_to_log", "0");
259 mMapper = createInputMapper<RotaryEncoderInputMapper>(*mDeviceContext, mReaderConfiguration,
260 mTelemetryLogCounter);
261 InputDeviceInfo info;
262 mMapper->populateDeviceInfo(info);
263
264 std::list<NotifyArgs> args;
265 args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, 700);
266 args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);
267
268 ASSERT_EQ(mTelemetryLogCounts.find("input.value_rotary_input_device_full_rotation_count"),
269 mTelemetryLogCounts.end());
270 }
271
TEST_F_WITH_FLAGS(RotaryEncoderInputMapperTest,NoMinLogRotation_NoRotationLogging,REQUIRES_FLAGS_ENABLED (ACONFIG_FLAG (com::android::input::flags,rotary_input_telemetry)))272 TEST_F_WITH_FLAGS(RotaryEncoderInputMapperTest, NoMinLogRotation_NoRotationLogging,
273 REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(com::android::input::flags,
274 rotary_input_telemetry))) {
275 // 3 units per radian, 2 * M_PI * 3 = ~18.85 units per rotation.
276 mPropertyMap.addProperty("device.res", "3");
277 mMapper = createInputMapper<RotaryEncoderInputMapper>(*mDeviceContext, mReaderConfiguration,
278 mTelemetryLogCounter);
279 InputDeviceInfo info;
280 mMapper->populateDeviceInfo(info);
281
282 std::list<NotifyArgs> args;
283 args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, 700);
284 args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);
285
286 ASSERT_EQ(mTelemetryLogCounts.find("input.value_rotary_input_device_full_rotation_count"),
287 mTelemetryLogCounts.end());
288 }
289
TEST_F_WITH_FLAGS(RotaryEncoderInputMapperTest,RotationLogging,REQUIRES_FLAGS_ENABLED (ACONFIG_FLAG (com::android::input::flags,rotary_input_telemetry)))290 TEST_F_WITH_FLAGS(RotaryEncoderInputMapperTest, RotationLogging,
291 REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(com::android::input::flags,
292 rotary_input_telemetry))) {
293 // 3 units per radian, 2 * M_PI * 3 = ~18.85 units per rotation.
294 // Multiples of `unitsPerRoation`, to easily follow the assertions below.
295 // [18.85, 37.7, 56.55, 75.4, 94.25, 113.1, 131.95, 150.8]
296 mPropertyMap.addProperty("device.res", "3");
297 mPropertyMap.addProperty("rotary_encoder.min_rotations_to_log", "2");
298
299 mMapper = createInputMapper<RotaryEncoderInputMapper>(*mDeviceContext, mReaderConfiguration,
300 mTelemetryLogCounter);
301 InputDeviceInfo info;
302 mMapper->populateDeviceInfo(info);
303
304 std::list<NotifyArgs> args;
305 args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, 15); // total scroll = 15
306 args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);
307 ASSERT_EQ(mTelemetryLogCounts.find("input.value_rotary_input_device_full_rotation_count"),
308 mTelemetryLogCounts.end());
309
310 args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, 13); // total scroll = 28
311 args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);
312 // Expect 0 since `min_rotations_to_log` = 2, and total scroll 28 only has 1 rotation.
313 ASSERT_EQ(mTelemetryLogCounts.find("input.value_rotary_input_device_full_rotation_count"),
314 mTelemetryLogCounts.end());
315
316 args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, 10); // total scroll = 38
317 args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);
318 // Total scroll includes >= `min_rotations_to_log` (2), expect log.
319 ASSERT_EQ(mTelemetryLogCounts["input.value_rotary_input_device_full_rotation_count"], 2);
320
321 args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, -22); // total scroll = 60
322 args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);
323 // Expect no additional telemetry. Total rotation is 3, and total unlogged rotation is 1, which
324 // is less than `min_rotations_to_log`.
325 ASSERT_EQ(mTelemetryLogCounts["input.value_rotary_input_device_full_rotation_count"], 2);
326
327 args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, -16); // total scroll = 76
328 args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);
329 // Total unlogged rotation >= `min_rotations_to_log` (2), so expect 2 more logged rotation.
330 ASSERT_EQ(mTelemetryLogCounts["input.value_rotary_input_device_full_rotation_count"], 4);
331
332 args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, -76); // total scroll = 152
333 args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);
334 // Total unlogged scroll >= 4*`min_rotations_to_log`. Expect *all* unlogged rotations to be
335 // logged, even if that's more than multiple of `min_rotations_to_log`.
336 ASSERT_EQ(mTelemetryLogCounts["input.value_rotary_input_device_full_rotation_count"], 8);
337 }
338
339 } // namespace android