blob: 6de8b181a3b24c0d4dd8e484b7d40ad09f7b3423 [file]
/*
* 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 <android-base/logging.h>
#include <gtest/gtest.h>
#include <list>
#include <optional>
#include "InputMapperTest.h"
#include "InterfaceMocks.h"
#include "TestEventMatchers.h"
#include "input/ScopedFlagOverride.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<uint8_t> 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);
} // namespace
/**
* Unit tests for MultiTouchInputMapper.
*/
class MultiTouchInputMapperUnitTest : public VerifyingInputMapperUnitTest {
protected:
void SetUp() override { SetUp(/*bus=*/0, /*isExternal=*/false); }
void SetUp(int bus, bool isExternal) override {
VerifyingInputMapperUnitTest::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<TouchVideoFrame>{}));
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<MultiTouchInputMapper>(*mDeviceContext,
mFakePolicy->getReaderConfiguration());
}
// Mocks position and tracking Ids for the provided slots. Remaining slots will be marked
// unused.
void mockSlotValues(
const std::unordered_map<int32_t /*slotIndex*/,
std::pair<Point /*position*/, int32_t /*trackingId*/>>&
slotValues) {
EXPECT_CALL(mMockEventHub, getMtSlotValues(EVENTHUB_ID, _, SLOT_COUNT))
.WillRepeatedly([=](int32_t, int32_t axis,
size_t slotCount) -> base::Result<std::vector<int32_t>> {
// tracking Id for the unused slots must set to be < 0
std::vector<int32_t> 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);
}
});
}
};
/**
* 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) {
// 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<DisplayViewport> 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));
reconfigureMapper(systemTime(SYSTEM_TIME_MONOTONIC), mReaderConfiguration,
InputReaderConfiguration::Change::DISPLAY_INFO);
int32_t x1 = 100, y1 = 125;
process(EV_KEY, BTN_TOUCH, 1);
process(EV_ABS, ABS_MT_POSITION_X, x1);
process(EV_ABS, ABS_MT_POSITION_Y, y1);
process(EV_ABS, ABS_MT_TRACKING_ID, 1);
process(EV_SYN, SYN_REPORT, 0);
mFakeListener.expectMotion(
AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), WithDisplayId(DISPLAY_ID)));
// Now associate with the second viewport, and reconfigure.
ON_CALL((*mDevice), getAssociatedViewport).WillByDefault(Return(secondViewport));
reconfigureMapper(systemTime(SYSTEM_TIME_MONOTONIC), mReaderConfiguration,
InputReaderConfiguration::Change::DISPLAY_INFO);
mFakeListener.expectMotion(AllOf(WithMotionAction(ACTION_CANCEL), WithDisplayId(DISPLAY_ID)));
mFakeListener.expectDeviceReset(WithDeviceId(DEVICE_ID));
// The remainder of the gesture is ignored
// Move.
x1 += 10;
y1 += 15;
process(EV_ABS, ABS_MT_POSITION_X, x1);
process(EV_ABS, ABS_MT_POSITION_Y, y1);
process(EV_SYN, SYN_REPORT, 0);
// Up
process(EV_KEY, BTN_TOUCH, 0);
process(EV_ABS, ABS_MT_TRACKING_ID, -1);
process(EV_SYN, SYN_REPORT, 0);
// New touch is delivered with the new display id.
process(EV_ABS, ABS_MT_TRACKING_ID, 2);
process(EV_KEY, BTN_TOUCH, 1);
process(EV_ABS, ABS_MT_POSITION_X, x1 + 20);
process(EV_ABS, ABS_MT_POSITION_Y, y1 + 40);
process(EV_SYN, SYN_REPORT, 0);
mFakeListener.expectMotion(
AllOf(WithMotionAction(AMOTION_EVENT_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) {
// 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;
process(EV_KEY, BTN_TOUCH, 1);
process(EV_ABS, ABS_MT_POSITION_X, x1);
process(EV_ABS, ABS_MT_POSITION_Y, y1);
process(EV_ABS, ABS_MT_TRACKING_ID, FIRST_TRACKING_ID);
process(EV_ABS, ABS_MT_SLOT, 1);
process(EV_ABS, ABS_MT_POSITION_X, x2);
process(EV_ABS, ABS_MT_POSITION_Y, y2);
process(EV_ABS, ABS_MT_TRACKING_ID, SECOND_TRACKING_ID);
process(EV_SYN, SYN_REPORT, 0);
mFakeListener.expectMotion((WithMotionAction(AMOTION_EVENT_ACTION_DOWN)));
mFakeListener.expectMotion((WithMotionAction(ACTION_POINTER_1_DOWN)));
// Move.
x1 += 10;
y1 += 15;
x2 += 5;
y2 -= 10;
process(EV_ABS, ABS_MT_SLOT, 0);
process(EV_ABS, ABS_MT_POSITION_X, x1);
process(EV_ABS, ABS_MT_POSITION_Y, y1);
process(EV_ABS, ABS_MT_SLOT, 1);
process(EV_ABS, ABS_MT_POSITION_X, x2);
process(EV_ABS, ABS_MT_POSITION_Y, y2);
process(EV_SYN, SYN_REPORT, 0);
NotifyMotionArgs args = mFakeListener.expectMotion(WithMotionAction(AMOTION_EVENT_ACTION_MOVE));
const auto pointerCoordsBeforeReset = args.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});
resetMapper(systemTime(SYSTEM_TIME_MONOTONIC));
mFakeListener.expectMotion(WithMotionAction(AMOTION_EVENT_ACTION_CANCEL));
// SYN_REPORT should restart the gesture again
process(EV_SYN, SYN_REPORT, 0);
mFakeListener.expectMotion(WithMotionAction(AMOTION_EVENT_ACTION_DOWN));
NotifyMotionArgs motion = mFakeListener.expectMotion(WithMotionAction(ACTION_POINTER_1_DOWN));
ASSERT_EQ(motion.pointerCoords, pointerCoordsBeforeReset);
// Move.
x1 += 10;
y1 += 15;
x2 += 5;
y2 -= 10;
process(EV_ABS, ABS_MT_SLOT, 0);
process(EV_ABS, ABS_MT_POSITION_X, x1);
process(EV_ABS, ABS_MT_POSITION_Y, y1);
process(EV_ABS, ABS_MT_SLOT, 1);
process(EV_ABS, ABS_MT_POSITION_X, x2);
process(EV_ABS, ABS_MT_POSITION_Y, y2);
process(EV_SYN, SYN_REPORT, 0);
mFakeListener.expectMotion(WithMotionAction(AMOTION_EVENT_ACTION_MOVE));
// First finger up.
process(EV_ABS, ABS_MT_SLOT, 0);
process(EV_ABS, ABS_MT_TRACKING_ID, -1);
process(EV_SYN, SYN_REPORT, 0);
mFakeListener.expectMotion(WithMotionAction(ACTION_POINTER_0_UP));
// Second finger up.
process(EV_KEY, BTN_TOUCH, 0);
process(EV_ABS, ABS_MT_SLOT, 1);
process(EV_ABS, ABS_MT_TRACKING_ID, -1);
process(EV_SYN, SYN_REPORT, 0);
mFakeListener.expectMotion(WithMotionAction(AMOTION_EVENT_ACTION_UP));
}
/**
* Check what happens when two pointers are hovering (BTN_TOUCH is not pressed).
*/
TEST_F(MultiTouchInputMapperUnitTest, TwoPointersHoveringWithoutBtnTouch) {
// Set up two pointers hovering (BTN_TOUCH is not pressed)
process(EV_ABS, ABS_MT_SLOT, 0);
process(EV_ABS, ABS_MT_TRACKING_ID, 0);
process(EV_ABS, ABS_MT_POSITION_X, 100);
process(EV_ABS, ABS_MT_POSITION_Y, 100);
process(EV_ABS, ABS_MT_SLOT, 1);
process(EV_ABS, ABS_MT_TRACKING_ID, 1);
process(EV_ABS, ABS_MT_POSITION_X, 200);
process(EV_ABS, ABS_MT_POSITION_Y, 200);
process(EV_SYN, SYN_REPORT, 0);
// In general, Android does not support two pointers hovering. No events should be produced,
// since both pointers are being added in the same frame here.
mFakeListener.assertNoEvents();
// Now lift pointer 0. Pointer 1 remains.
process(EV_ABS, ABS_MT_SLOT, 0);
process(EV_ABS, ABS_MT_TRACKING_ID, -1);
process(EV_SYN, SYN_REPORT, 0);
// We expect HOVER_MOVE with 1 pointer (pointer 1)
// Since we previously dropped 2 pointers, this looks like we are transitioning from 0 to 1
// hovering pointer.
mFakeListener.expectMotion(AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER),
WithPointerCount(1), WithPointerId(0, 1)));
mFakeListener.expectMotion(AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE),
WithPointerCount(1), WithPointerId(0, 1)));
}
/**
* Check what happens when two pointers are hovering (pressure is 0).
*/
TEST_F(MultiTouchInputMapperUnitTest, TwoPointersHoveringWithPressure) {
// Reconfigure the device to support pressure
setupAxis(ABS_MT_PRESSURE, /*valid=*/true, /*min=*/0, /*max=*/255, /*resolution=*/0);
mMapper = createInputMapper<MultiTouchInputMapper>(*mDeviceContext,
mFakePolicy->getReaderConfiguration());
// Press BTN_TOUCH, but pressure is 0. This should cause the pointers to hover.
process(EV_KEY, BTN_TOUCH, 1);
// Pointer 0: Hovering (pressure 0)
process(EV_ABS, ABS_MT_SLOT, 0);
process(EV_ABS, ABS_MT_TRACKING_ID, 0);
process(EV_ABS, ABS_MT_POSITION_X, 100);
process(EV_ABS, ABS_MT_POSITION_Y, 100);
process(EV_ABS, ABS_MT_PRESSURE, 0);
// Pointer 1: Hovering (pressure 0)
process(EV_ABS, ABS_MT_SLOT, 1);
process(EV_ABS, ABS_MT_TRACKING_ID, 1);
process(EV_ABS, ABS_MT_POSITION_X, 200);
process(EV_ABS, ABS_MT_POSITION_Y, 200);
process(EV_ABS, ABS_MT_PRESSURE, 0);
process(EV_SYN, SYN_REPORT, 0);
// In general, Android does not support two pointers hovering. No events should be produced,
// since both pointers are being added in the same frame here.
mFakeListener.assertNoEvents();
}
/**
* Start with one hovering pointer, and then add a second one. Ensure that the second hovering
* pointer is not reported, because Android does not support multiple hovering pointers.
*/
TEST_F(MultiTouchInputMapperUnitTest, OneToTwoPointersHovering) {
// Start with 1 pointer hovering (P0)
process(EV_ABS, ABS_MT_SLOT, 0);
process(EV_ABS, ABS_MT_TRACKING_ID, 0);
process(EV_ABS, ABS_MT_POSITION_X, 100);
process(EV_ABS, ABS_MT_POSITION_Y, 100);
process(EV_SYN, SYN_REPORT, 0);
// Expect HOVER_ENTER {0}
mFakeListener.expectMotion(AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER),
WithPointerCount(1), WithPointerId(0, 0)));
mFakeListener.expectMotion(AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE),
WithPointerCount(1), WithPointerId(0, 0)));
// Add second pointer hovering (P1). Now have P0, P1.
process(EV_ABS, ABS_MT_SLOT, 1);
process(EV_ABS, ABS_MT_TRACKING_ID, 1);
process(EV_ABS, ABS_MT_POSITION_X, 200);
process(EV_ABS, ABS_MT_POSITION_Y, 200);
process(EV_SYN, SYN_REPORT, 0);
// Expect HOVER_EXIT {0} because multi-pointer hover is suppressed.
// The mapper detects >1 pointers, clears them, so cooked state becomes empty.
// Transition from {0} to {} triggers HOVER_EXIT {0}.
mFakeListener.expectMotion(AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_EXIT),
WithPointerCount(1), WithPointerId(0, 0)));
// Move pointers (P0, P1).
process(EV_ABS, ABS_MT_SLOT, 0);
process(EV_ABS, ABS_MT_POSITION_X, 110);
process(EV_ABS, ABS_MT_POSITION_Y, 110);
process(EV_SYN, SYN_REPORT, 0);
// Still suppressed.
mFakeListener.assertNoEvents();
// Lift P0. P1 remains.
process(EV_ABS, ABS_MT_SLOT, 0);
process(EV_ABS, ABS_MT_TRACKING_ID, -1);
process(EV_SYN, SYN_REPORT, 0);
// Transition from {} to {1}. Expect HOVER_ENTER {1}.
mFakeListener.expectMotion(AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER),
WithPointerCount(1), WithPointerId(0, 1)));
mFakeListener.expectMotion(AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE),
WithPointerCount(1), WithPointerId(0, 1)));
}
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) {
// Expect the event to be sent to the internal viewport,
// because an external viewport is not present.
process(EV_KEY, BTN_TOUCH, 1);
process(EV_ABS, ABS_MT_TRACKING_ID, 1);
process(EV_ABS, ABS_MT_POSITION_X, 100);
process(EV_ABS, ABS_MT_POSITION_Y, 200);
process(EV_SYN, SYN_REPORT, 0);
mFakeListener.expectMotion(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<DisplayViewport> internalViewport =
mFakePolicy->getDisplayViewportByUniqueId("local:0");
mReaderConfiguration.setDisplayViewports({*internalViewport, externalViewport});
reconfigureMapper(systemTime(SYSTEM_TIME_MONOTONIC), mReaderConfiguration,
InputReaderConfiguration::Change::DISPLAY_INFO);
mFakeListener.expectMotion(AllOf(WithMotionAction(ACTION_CANCEL), WithDisplayId(DISPLAY_ID)));
mFakeListener.expectDeviceReset(WithDeviceId(DEVICE_ID));
// Lift up the old pointer.
process(EV_KEY, BTN_TOUCH, 0);
process(EV_ABS, ABS_MT_TRACKING_ID, -1);
process(EV_SYN, SYN_REPORT, 0);
// Send new pointer
process(EV_KEY, BTN_TOUCH, 1);
process(EV_ABS, ABS_MT_TRACKING_ID, 2);
process(EV_ABS, ABS_MT_POSITION_X, 111);
process(EV_ABS, ABS_MT_POSITION_Y, 211);
process(EV_SYN, SYN_REPORT, 0);
mFakeListener.expectMotion(
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<MultiTouchInputMapper>(*mDeviceContext,
mFakePolicy->getReaderConfiguration());
}
};
TEST_F(MultiTouchInputMapperPointerModeUnitTest, MouseToolOnlyDownWhenMouseButtonsAreDown) {
// Set the tool type to mouse.
process(EV_KEY, BTN_TOOL_MOUSE, 1);
process(EV_ABS, ABS_MT_POSITION_X, 100);
process(EV_ABS, ABS_MT_POSITION_Y, 100);
process(EV_ABS, ABS_MT_TRACKING_ID, 1);
process(EV_SYN, SYN_REPORT, 0);
mFakeListener.expectMotion(AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER),
WithToolType(ToolType::MOUSE)));
mFakeListener.expectMotion(AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE),
WithToolType(ToolType::MOUSE)));
// Setting BTN_TOUCH does not make a mouse pointer go down.
process(EV_KEY, BTN_TOUCH, 1);
process(EV_SYN, SYN_REPORT, 0);
mFakeListener.expectMotion(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_MOVE));
// The mouse button is pressed, so the mouse goes down.
process(EV_KEY, BTN_MOUSE, 1);
process(EV_SYN, SYN_REPORT, 0);
mFakeListener.expectMotion(AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_EXIT),
WithToolType(ToolType::MOUSE)));
mFakeListener.expectMotion(AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN),
WithToolType(ToolType::MOUSE),
WithButtonState(AMOTION_EVENT_BUTTON_PRIMARY)));
mFakeListener.expectMotion(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.
process(EV_KEY, BTN_MOUSE, 0);
process(EV_SYN, SYN_REPORT, 0);
mFakeListener.expectMotion(AllOf(WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_RELEASE),
WithButtonState(0), WithToolType(ToolType::MOUSE),
WithActionButton(AMOTION_EVENT_BUTTON_PRIMARY)));
mFakeListener.expectMotion(AllOf(WithMotionAction(AMOTION_EVENT_ACTION_UP),
WithToolType(ToolType::MOUSE), WithButtonState(0)));
mFakeListener.expectMotion(AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER),
WithToolType(ToolType::MOUSE)));
mFakeListener.expectMotion(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.
process(EV_KEY, BTN_TOOL_MOUSE, 0);
process(EV_SYN, SYN_REPORT, 0);
mFakeListener.expectMotion(AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_EXIT),
WithToolType(ToolType::MOUSE)));
mFakeListener.expectMotion(
AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), WithToolType(ToolType::FINGER)));
}
} // namespace android