/* * Copyright 2024 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 "MultiTouchInputMapper.h" #include #include #include #include #include "InputMapperTest.h" #include "InterfaceMocks.h" #include "ScopedFlagOverride.h" #include "TestEventMatchers.h" #define TAG "MultiTouchpadInputMapperUnit_test" namespace android { using testing::_; using testing::AllOf; using testing::IsEmpty; using testing::Return; using testing::SetArgPointee; using testing::VariantWith; namespace { constexpr ui::LogicalDisplayId DISPLAY_ID = ui::LogicalDisplayId::DEFAULT; constexpr ui::LogicalDisplayId SECOND_DISPLAY_ID = ui::LogicalDisplayId{DISPLAY_ID.val() + 1}; constexpr int32_t DISPLAY_WIDTH = 480; constexpr int32_t DISPLAY_HEIGHT = 800; constexpr std::optional NO_PORT = std::nullopt; // no physical port is specified constexpr int32_t SLOT_COUNT = 5; constexpr int32_t ACTION_DOWN = AMOTION_EVENT_ACTION_DOWN; constexpr int32_t ACTION_CANCEL = AMOTION_EVENT_ACTION_CANCEL; constexpr int32_t ACTION_POINTER_0_UP = AMOTION_EVENT_ACTION_POINTER_UP | (0 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT); constexpr int32_t ACTION_POINTER_1_DOWN = AMOTION_EVENT_ACTION_POINTER_DOWN | (1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT); template void assertNotifyArgs(const std::list& args, Args... matchers) { ASSERT_THAT(args, ElementsAre(matchers...)) << "Got instead: " << dumpContainer(args, streamableToString); } } // namespace /** * Unit tests for MultiTouchInputMapper. */ class MultiTouchInputMapperUnitTest : public InputMapperUnitTest { protected: void SetUp() override { SetUp(/*bus=*/0, /*isExternal=*/false); } void SetUp(int bus, bool isExternal) override { InputMapperUnitTest::SetUp(bus, isExternal); // Present scan codes expectScanCodes(/*present=*/true, {BTN_TOUCH, BTN_TOOL_FINGER, BTN_TOOL_DOUBLETAP, BTN_TOOL_TRIPLETAP, BTN_TOOL_QUADTAP, BTN_TOOL_QUINTTAP}); // Missing scan codes that the mapper checks for. expectScanCodes(/*present=*/false, {BTN_TOOL_PEN, BTN_TOOL_RUBBER, BTN_TOOL_BRUSH, BTN_TOOL_PENCIL, BTN_TOOL_AIRBRUSH}); // Current scan code state - all keys are UP by default setScanCodeState(KeyState::UP, {BTN_LEFT, BTN_RIGHT, BTN_MIDDLE, BTN_BACK, BTN_SIDE, BTN_FORWARD, BTN_EXTRA, BTN_TASK, BTN_TOUCH, BTN_STYLUS, BTN_STYLUS2, BTN_0, BTN_TOOL_FINGER, BTN_TOOL_PEN, BTN_TOOL_RUBBER, BTN_TOOL_BRUSH, BTN_TOOL_PENCIL, BTN_TOOL_AIRBRUSH, BTN_TOOL_MOUSE, BTN_TOOL_LENS, BTN_TOOL_DOUBLETAP, BTN_TOOL_TRIPLETAP, BTN_TOOL_QUADTAP, BTN_TOOL_QUINTTAP}); setKeyCodeState(KeyState::UP, {AKEYCODE_STYLUS_BUTTON_PRIMARY, AKEYCODE_STYLUS_BUTTON_SECONDARY}); // Input properties - only INPUT_PROP_DIRECT for touchscreen EXPECT_CALL(mMockEventHub, hasInputProperty(EVENTHUB_ID, _)).WillRepeatedly(Return(false)); EXPECT_CALL(mMockEventHub, hasInputProperty(EVENTHUB_ID, INPUT_PROP_DIRECT)) .WillRepeatedly(Return(true)); // The following EXPECT_CALL lines are not load-bearing, but without them gtest prints // warnings about "uninteresting mocked call", which are distracting when developing the // tests because this text is interleaved with logs of interest. EXPECT_CALL(mMockEventHub, getVirtualKeyDefinitions(EVENTHUB_ID, _)) .WillRepeatedly(Return()); EXPECT_CALL(mMockEventHub, hasRelativeAxis(EVENTHUB_ID, _)) .WillRepeatedly(testing::Return(false)); EXPECT_CALL(mMockEventHub, getVideoFrames(EVENTHUB_ID)) .WillRepeatedly(testing::Return(std::vector{})); EXPECT_CALL(mMockInputReaderContext, getExternalStylusDevices(_)).WillRepeatedly(Return()); EXPECT_CALL(mMockInputReaderContext, getGlobalMetaState()).WillRepeatedly(Return(0)); // Axes that the device has setupAxis(ABS_MT_SLOT, /*valid=*/true, /*min=*/0, /*max=*/SLOT_COUNT - 1, /*resolution=*/0); setupAxis(ABS_MT_TRACKING_ID, /*valid=*/true, /*min*/ 0, /*max=*/255, /*resolution=*/0); setupAxis(ABS_MT_POSITION_X, /*valid=*/true, /*min=*/0, /*max=*/2000, /*resolution=*/24); setupAxis(ABS_MT_POSITION_Y, /*valid=*/true, /*min=*/0, /*max=*/1000, /*resolution=*/24); // Axes that the device does not have setupAxis(ABS_MT_PRESSURE, /*valid=*/false, /*min*/ 0, /*max=*/255, /*resolution=*/0); setupAxis(ABS_MT_ORIENTATION, /*valid=*/false, /*min=*/0, /*max=*/0, /*resolution=*/0); setupAxis(ABS_MT_DISTANCE, /*valid=*/false, /*min=*/0, /*max=*/0, /*resolution=*/0); setupAxis(ABS_MT_TOUCH_MAJOR, /*valid=*/false, /*min=*/0, /*max=*/0, /*resolution=*/0); setupAxis(ABS_MT_TOUCH_MINOR, /*valid=*/false, /*min=*/0, /*max=*/0, /*resolution=*/0); setupAxis(ABS_MT_WIDTH_MAJOR, /*valid=*/false, /*min=*/0, /*max=*/0, /*resolution=*/0); setupAxis(ABS_MT_WIDTH_MINOR, /*valid=*/false, /*min=*/0, /*max=*/0, /*resolution=*/0); setupAxis(ABS_MT_TOOL_TYPE, /*valid=*/false, /*min=*/0, /*max=*/0, /*resolution=*/0); // reset current slot at the beginning EXPECT_CALL(mMockEventHub, getAbsoluteAxisValue(EVENTHUB_ID, ABS_MT_SLOT)) .WillRepeatedly(Return(0)); // mark all slots not in use mockSlotValues({}); mFakePolicy->setDefaultPointerDisplayId(DISPLAY_ID); DisplayViewport internalViewport = createViewport(DISPLAY_ID, DISPLAY_WIDTH, DISPLAY_HEIGHT, ui::ROTATION_0, /*isActive=*/true, "local:0", NO_PORT, ViewportType::INTERNAL); mFakePolicy->addDisplayViewport(internalViewport); mMapper = createInputMapper(*mDeviceContext, mFakePolicy->getReaderConfiguration()); } // Mocks position and tracking Ids for the provided slots. Remaining slots will be marked // unused. void mockSlotValues( const std::unordered_map>& slotValues) { EXPECT_CALL(mMockEventHub, getMtSlotValues(EVENTHUB_ID, _, SLOT_COUNT)) .WillRepeatedly([=](int32_t, int32_t axis, size_t slotCount) -> base::Result> { // tracking Id for the unused slots must set to be < 0 std::vector outMtSlotValues(slotCount + 1, -1); outMtSlotValues[0] = axis; switch (axis) { case ABS_MT_POSITION_X: for (const auto& [slotIndex, valuePair] : slotValues) { outMtSlotValues[slotIndex] = valuePair.first.x; } return outMtSlotValues; case ABS_MT_POSITION_Y: for (const auto& [slotIndex, valuePair] : slotValues) { outMtSlotValues[slotIndex] = valuePair.first.y; } return outMtSlotValues; case ABS_MT_TRACKING_ID: for (const auto& [slotIndex, valuePair] : slotValues) { outMtSlotValues[slotIndex] = valuePair.second; } return outMtSlotValues; default: return base::ResultError("Axis not supported", NAME_NOT_FOUND); } }); } [[nodiscard]] std::list processPosition(int32_t x, int32_t y) { std::list args; args += process(EV_ABS, ABS_MT_POSITION_X, x); args += process(EV_ABS, ABS_MT_POSITION_Y, y); return args; } [[nodiscard]] std::list processId(int32_t id) { return process(EV_ABS, ABS_MT_TRACKING_ID, id); } [[nodiscard]] std::list processKey(int32_t code, int32_t value) { return process(EV_KEY, code, value); } [[nodiscard]] std::list processSlot(int32_t slot) { return process(EV_ABS, ABS_MT_SLOT, slot); } [[nodiscard]] std::list processSync() { return process(EV_SYN, SYN_REPORT, 0); } }; /** * While a gesture is active, change the display that the device is associated with. Make sure that * the CANCEL event that's generated has the display id of the original DOWN event, rather than the * new display id. */ TEST_F(MultiTouchInputMapperUnitTest, ChangeAssociatedDisplayIdWhenTouchIsActive) { std::list args; // Add a second viewport that later will be associated with our device. DisplayViewport secondViewport = createViewport(SECOND_DISPLAY_ID, DISPLAY_WIDTH, DISPLAY_HEIGHT, ui::ROTATION_0, /*isActive=*/true, "local:1", NO_PORT, ViewportType::EXTERNAL); mFakePolicy->addDisplayViewport(secondViewport); std::optional firstViewport = mFakePolicy->getDisplayViewportByUniqueId("local:0"); // InputReaderConfiguration contains information about how devices are associated with displays. // The mapper receives this information. However, it doesn't actually parse it - that's done by // InputDevice. The mapper asks InputDevice about the associated viewport, so that's what we // need to mock here to simulate association. This abstraction is confusing and should be // refactored. // Start with the first viewport ON_CALL((*mDevice), getAssociatedViewport).WillByDefault(Return(firstViewport)); args += mMapper->reconfigure(systemTime(SYSTEM_TIME_MONOTONIC), mReaderConfiguration, InputReaderConfiguration::Change::DISPLAY_INFO); int32_t x1 = 100, y1 = 125; args += processKey(BTN_TOUCH, 1); args += processPosition(x1, y1); args += processId(1); args += processSync(); ASSERT_THAT(args, ElementsAre(VariantWith( AllOf(WithMotionAction(ACTION_DOWN), WithDisplayId(DISPLAY_ID))))); args.clear(); // Now associate with the second viewport, and reconfigure. ON_CALL((*mDevice), getAssociatedViewport).WillByDefault(Return(secondViewport)); args += mMapper->reconfigure(systemTime(SYSTEM_TIME_MONOTONIC), mReaderConfiguration, InputReaderConfiguration::Change::DISPLAY_INFO); assertNotifyArgs(args, VariantWith( AllOf(WithMotionAction(ACTION_CANCEL), WithDisplayId(DISPLAY_ID))), VariantWith(WithDeviceId(DEVICE_ID))); // The remainder of the gesture is ignored // Move. x1 += 10; y1 += 15; args = processPosition(x1, y1); args += processSync(); // Up args += processKey(BTN_TOUCH, 0); args += processId(-1); args += processSync(); ASSERT_THAT(args, IsEmpty()); // New touch is delivered with the new display id. args += processId(2); args += processKey(BTN_TOUCH, 1); args += processPosition(x1 + 20, y1 + 40); args += processSync(); assertNotifyArgs(args, VariantWith(AllOf(WithMotionAction(ACTION_DOWN), WithDisplayId(SECOND_DISPLAY_ID)))); } // This test simulates a multi-finger gesture with unexpected reset in between. This might happen // due to buffer overflow and device with report a SYN_DROPPED. In this case we expect mapper to be // reset, MT slot state to be re-populated and the gesture should be cancelled and restarted. TEST_F(MultiTouchInputMapperUnitTest, MultiFingerGestureWithUnexpectedReset) { std::list args; // Two fingers down at once. constexpr int32_t FIRST_TRACKING_ID = 1, SECOND_TRACKING_ID = 2; int32_t x1 = 100, y1 = 125, x2 = 200, y2 = 225; args += processKey(BTN_TOUCH, 1); args += processPosition(x1, y1); args += processId(FIRST_TRACKING_ID); args += processSlot(1); args += processPosition(x2, y2); args += processId(SECOND_TRACKING_ID); ASSERT_THAT(args, IsEmpty()); args += processSync(); ASSERT_THAT(args, ElementsAre(VariantWith( WithMotionAction(AMOTION_EVENT_ACTION_DOWN)), VariantWith( WithMotionAction(ACTION_POINTER_1_DOWN)))); // Move. x1 += 10; y1 += 15; x2 += 5; y2 -= 10; args = processSlot(0); args += processPosition(x1, y1); args += processSlot(1); args += processPosition(x2, y2); ASSERT_THAT(args, IsEmpty()); args = processSync(); ASSERT_THAT(args, ElementsAre(VariantWith( WithMotionAction(AMOTION_EVENT_ACTION_MOVE)))); const auto pointerCoordsBeforeReset = std::get(args.back()).pointerCoords; // On buffer overflow mapper will be reset and MT slots data will be repopulated EXPECT_CALL(mMockEventHub, getAbsoluteAxisValue(EVENTHUB_ID, ABS_MT_SLOT)) .WillRepeatedly(Return(1)); mockSlotValues( {{1, {Point{x1, y1}, FIRST_TRACKING_ID}}, {2, {Point{x2, y2}, SECOND_TRACKING_ID}}}); setScanCodeState(KeyState::DOWN, {BTN_TOUCH}); args = mMapper->reset(systemTime(SYSTEM_TIME_MONOTONIC)); ASSERT_THAT(args, ElementsAre(VariantWith( WithMotionAction(AMOTION_EVENT_ACTION_CANCEL)))); // SYN_REPORT should restart the gesture again args = processSync(); ASSERT_THAT(args, ElementsAre(VariantWith( WithMotionAction(AMOTION_EVENT_ACTION_DOWN)), VariantWith( WithMotionAction(ACTION_POINTER_1_DOWN)))); ASSERT_EQ(std::get(args.back()).pointerCoords, pointerCoordsBeforeReset); // Move. x1 += 10; y1 += 15; x2 += 5; y2 -= 10; args = processSlot(0); args += processPosition(x1, y1); args += processSlot(1); args += processPosition(x2, y2); ASSERT_THAT(args, IsEmpty()); args = processSync(); ASSERT_THAT(args, ElementsAre(VariantWith( WithMotionAction(AMOTION_EVENT_ACTION_MOVE)))); // First finger up. args = processSlot(0); args += processId(-1); ASSERT_THAT(args, IsEmpty()); args = processSync(); ASSERT_THAT(args, ElementsAre(VariantWith(WithMotionAction(ACTION_POINTER_0_UP)))); // Second finger up. args = processKey(BTN_TOUCH, 0); args += processSlot(1); args += processId(-1); ASSERT_THAT(args, IsEmpty()); args = processSync(); ASSERT_THAT(args, ElementsAre( VariantWith(WithMotionAction(AMOTION_EVENT_ACTION_UP)))); } class ExternalMultiTouchInputMapperTest : public MultiTouchInputMapperUnitTest { protected: void SetUp() override { MultiTouchInputMapperUnitTest::SetUp(/*bus=*/0, /*isExternal=*/true); } }; /** * Expect fallback to internal viewport if device is external and external viewport is not present. */ TEST_F(ExternalMultiTouchInputMapperTest, Viewports_Fallback) { std::list args; // Expect the event to be sent to the internal viewport, // because an external viewport is not present. args += processKey(BTN_TOUCH, 1); args += processId(1); args += processPosition(100, 200); args += processSync(); assertNotifyArgs(args, VariantWith( AllOf(WithMotionAction(ACTION_DOWN), WithDisplayId(DISPLAY_ID)))); // Expect the event to be sent to the external viewport if it is present. DisplayViewport externalViewport = createViewport(SECOND_DISPLAY_ID, DISPLAY_WIDTH, DISPLAY_HEIGHT, ui::ROTATION_0, /*isActive=*/true, "local:1", NO_PORT, ViewportType::EXTERNAL); mFakePolicy->addDisplayViewport(externalViewport); std::optional internalViewport = mFakePolicy->getDisplayViewportByUniqueId("local:0"); mReaderConfiguration.setDisplayViewports({*internalViewport, externalViewport}); args = mMapper->reconfigure(systemTime(SYSTEM_TIME_MONOTONIC), mReaderConfiguration, InputReaderConfiguration::Change::DISPLAY_INFO); assertNotifyArgs(args, VariantWith( AllOf(WithMotionAction(ACTION_CANCEL), WithDisplayId(DISPLAY_ID))), VariantWith(WithDeviceId(DEVICE_ID))); // Lift up the old pointer. args = processKey(BTN_TOUCH, 0); args += processId(-1); args += processSync(); // Send new pointer args += processKey(BTN_TOUCH, 1); args += processId(2); args += processPosition(111, 211); args += processSync(); assertNotifyArgs(args, VariantWith(AllOf(WithMotionAction(ACTION_DOWN), WithDisplayId(SECOND_DISPLAY_ID)))); } class MultiTouchInputMapperPointerModeUnitTest : public MultiTouchInputMapperUnitTest { protected: void SetUp() override { MultiTouchInputMapperUnitTest::SetUp(); // TouchInputMapper goes into POINTER mode whenever INPUT_PROP_DIRECT is not set. EXPECT_CALL(mMockEventHub, hasInputProperty(EVENTHUB_ID, INPUT_PROP_DIRECT)) .WillRepeatedly(Return(false)); mMapper = createInputMapper(*mDeviceContext, mFakePolicy->getReaderConfiguration()); } }; TEST_F(MultiTouchInputMapperPointerModeUnitTest, MouseToolOnlyDownWhenMouseButtonsAreDown) { SCOPED_FLAG_OVERRIDE(disable_touch_input_mapper_pointer_usage, true); std::list args; // Set the tool type to mouse. args += processKey(BTN_TOOL_MOUSE, 1); args += processPosition(100, 100); args += processId(1); ASSERT_THAT(args, IsEmpty()); args = processSync(); ASSERT_THAT(args, ElementsAre(VariantWith( AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER), WithToolType(ToolType::MOUSE))), VariantWith( AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE), WithToolType(ToolType::MOUSE))))); // Setting BTN_TOUCH does not make a mouse pointer go down. args = processKey(BTN_TOUCH, 1); args += processSync(); ASSERT_THAT(args, ElementsAre(VariantWith( WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE)))); // The mouse button is pressed, so the mouse goes down. args = processKey(BTN_MOUSE, 1); args += processSync(); ASSERT_THAT(args, ElementsAre(VariantWith( AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_EXIT), WithToolType(ToolType::MOUSE))), VariantWith( AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), WithToolType(ToolType::MOUSE), WithButtonState(AMOTION_EVENT_BUTTON_PRIMARY))), VariantWith( AllOf(WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_PRESS), WithToolType(ToolType::MOUSE), WithButtonState(AMOTION_EVENT_BUTTON_PRIMARY), WithActionButton(AMOTION_EVENT_BUTTON_PRIMARY))))); // The mouse button is released, so the mouse starts hovering. args = processKey(BTN_MOUSE, 0); args += processSync(); ASSERT_THAT(args, ElementsAre(VariantWith( AllOf(WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_RELEASE), WithButtonState(0), WithToolType(ToolType::MOUSE), WithActionButton(AMOTION_EVENT_BUTTON_PRIMARY))), VariantWith( AllOf(WithMotionAction(AMOTION_EVENT_ACTION_UP), WithToolType(ToolType::MOUSE), WithButtonState(0))), VariantWith( AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER), WithToolType(ToolType::MOUSE))), VariantWith( AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE), WithToolType(ToolType::MOUSE))))); // Change the tool type so that it is no longer a mouse. // The default tool type is finger, and the finger is already down. args = processKey(BTN_TOOL_MOUSE, 0); args += processSync(); ASSERT_THAT(args, ElementsAre(VariantWith( AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_EXIT), WithToolType(ToolType::MOUSE))), VariantWith( AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), WithToolType(ToolType::FINGER))))); } } // namespace android