Vladimir Komsiyski | dd438e2 | 2024-02-13 11:47:54 +0100 | [diff] [blame] | 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> |
Biswarup Pal | ba27d1d | 2024-07-09 19:57:33 +0000 | [diff] [blame] | 25 | #include <android_companion_virtualdevice_flags.h> |
Yeabkal Wubshit | 58bda65 | 2024-09-24 20:22:18 -0700 | [diff] [blame] | 26 | #include <com_android_input_flags.h> |
| 27 | #include <flag_macros.h> |
Vladimir Komsiyski | dd438e2 | 2024-02-13 11:47:54 +0100 | [diff] [blame] | 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 | |
| 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 | |
| 68 | DisplayViewport createPrimaryViewport() { |
| 69 | DisplayViewport v = createViewport(); |
| 70 | v.displayId = DISPLAY_ID; |
| 71 | v.uniqueId = "local:1"; |
| 72 | return v; |
| 73 | } |
| 74 | |
| 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 | |
Vladimir Komsiyski | dd438e2 | 2024-02-13 11:47:54 +0100 | [diff] [blame] | 83 | } // namespace |
| 84 | |
Biswarup Pal | ba27d1d | 2024-07-09 19:57:33 +0000 | [diff] [blame] | 85 | namespace vd_flags = android::companion::virtualdevice::flags; |
| 86 | |
Vladimir Komsiyski | dd438e2 | 2024-02-13 11:47:54 +0100 | [diff] [blame] | 87 | /** |
| 88 | * Unit tests for RotaryEncoderInputMapper. |
| 89 | */ |
| 90 | class RotaryEncoderInputMapperTest : public InputMapperUnitTest { |
| 91 | protected: |
| 92 | void SetUp() override { SetUpWithBus(BUS_USB); } |
| 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)); |
Biswarup Pal | 8ff5e5e | 2024-06-15 12:58:20 +0000 | [diff] [blame] | 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)); |
Vladimir Komsiyski | dd438e2 | 2024-02-13 11:47:54 +0100 | [diff] [blame] | 104 | } |
Yeabkal Wubshit | 58bda65 | 2024-09-24 20:22:18 -0700 | [diff] [blame] | 105 | |
Yeabkal Wubshit | 12c4ce6 | 2024-10-02 08:10:19 +0000 | [diff] [blame] | 106 | std::map<std::string, int64_t> mTelemetryLogCounts; |
Yeabkal Wubshit | 58bda65 | 2024-09-24 20:22:18 -0700 | [diff] [blame] | 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 = |
| 113 | [this](const char* key, int64_t value) { mTelemetryLogCounts[key] += value; }; |
Vladimir Komsiyski | dd438e2 | 2024-02-13 11:47:54 +0100 | [diff] [blame] | 114 | }; |
| 115 | |
| 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. |
Harry Cutts | 0b613dc | 2024-07-25 19:33:48 +0000 | [diff] [blame] | 122 | EXPECT_CALL((*mDevice), getAssociatedViewport).WillRepeatedly(Return(secondaryViewport)); |
| 123 | mMapper = createInputMapper<RotaryEncoderInputMapper>(*mDeviceContext, mReaderConfiguration); |
Vladimir Komsiyski | dd438e2 | 2024-02-13 11:47:54 +0100 | [diff] [blame] | 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 | |
| 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. |
Vladimir Komsiyski | dd438e2 | 2024-02-13 11:47:54 +0100 | [diff] [blame] | 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 | |
Biswarup Pal | ba27d1d | 2024-07-09 19:57:33 +0000 | [diff] [blame] | 155 | TEST_F(RotaryEncoderInputMapperTest, ProcessRegularScroll) { |
Biswarup Pal | ba27d1d | 2024-07-09 19:57:33 +0000 | [diff] [blame] | 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 | |
| 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)); |
Biswarup Pal | ba27d1d | 2024-07-09 19:57:33 +0000 | [diff] [blame] | 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 | |
| 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)); |
Biswarup Pal | ba27d1d | 2024-07-09 19:57:33 +0000 | [diff] [blame] | 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 | |
Yeabkal Wubshit | 58bda65 | 2024-09-24 20:22:18 -0700 | [diff] [blame] | 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
Vladimir Komsiyski | dd438e2 | 2024-02-13 11:47:54 +0100 | [diff] [blame] | 339 | } // namespace android |