blob: 5ccdfea07700cc8d01979502e7de47852c5933a5 [file] [log] [blame]
// Copyright 2023 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <fidl/fuchsia.input.report/cpp/fidl.h>
#include <fidl/fuchsia.process/cpp/fidl.h>
#include <fidl/fuchsia.sysmem/cpp/fidl.h>
#include <fidl/fuchsia.sysmem2/cpp/fidl.h>
#include <fidl/fuchsia.ui.app/cpp/fidl.h>
#include <fidl/fuchsia.ui.composition/cpp/fidl.h>
#include <fidl/fuchsia.ui.input/cpp/fidl.h>
#include <fidl/fuchsia.ui.pointer/cpp/fidl.h>
#include <fidl/fuchsia.ui.pointer/cpp/natural_ostream.h>
#include <fidl/fuchsia.ui.test.input/cpp/fidl.h>
#include <lib/stdcompat/source_location.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/zx/result.h>
#include <lib/zx/time.h>
#include <zircon/status.h>
#include <zircon/time.h>
#include <zircon/types.h>
#include <cstddef>
#include <cstdint>
#include <sstream>
#include <vector>
#include <gtest/gtest.h>
#include "relay-api.h"
#include "src/ui/testing/util/portable_ui_test.h"
#include "src/ui/tests/integration_input_tests/starnix-input/starnix-input-test-base.h"
#include "third_party/android/platform/bionic/libc/kernel/uapi/linux/input-event-codes.h"
namespace {
// Maximum distance between two physical pixel coordinates so that they are considered equal.
constexpr double kEpsilon = 0.5f;
// Touch down is expressed in two `TouchEvents`s: btn_touch, Phase Add.
// Touch up is expressed in two: btn_touch, Phase Remove.
constexpr size_t kDownUpNumEvents = 4;
// Touch move is expressed in one `TouchEvents`: Phase Change.
// Touch up is expressed in two: btn_touch, Phase Remove.
constexpr size_t kMoveUpNumEvents = 3;
struct TouchEvent {
float local_x; // The x-position, in the client's coordinate space.
float local_y; // The y-position, in the client's coordinate space.
fuchsia_ui_pointer::EventPhase phase;
int slot_id;
int pointer_id; // only phase add include this field.
bool has_btn_touch; // only btn_touch event will only have this field.
int btn_touch; // only btn_touch event will only have this field.
};
void ExpectLocationPhaseAndSlot(
const TouchEvent& e, double expected_x, double expected_y,
fuchsia_ui_pointer::EventPhase expected_phase, int expected_slot_id,
const cpp20::source_location caller = cpp20::source_location::current()) {
std::string caller_info = "line " + std::to_string(caller.line());
EXPECT_EQ(expected_slot_id, e.slot_id) << " from " << caller_info;
EXPECT_EQ(expected_phase, e.phase) << " from " << caller_info;
EXPECT_NEAR(expected_x, e.local_x, kEpsilon) << " from " << caller_info;
EXPECT_NEAR(expected_y, e.local_y, kEpsilon) << " from " << caller_info;
EXPECT_EQ(false, e.has_btn_touch) << " from " << caller_info;
}
void ExpectBtnTouch(const TouchEvent& e, int expected_value,
const cpp20::source_location caller = cpp20::source_location::current()) {
std::string caller_info = "line " + std::to_string(caller.line());
EXPECT_EQ(true, e.has_btn_touch) << " from " << caller_info;
EXPECT_EQ(expected_value, e.btn_touch) << " from " << caller_info;
}
enum class TapLocation { kTopLeft, kBottomRight };
class StarnixTouchTest : public starnix_input_test::StarnixInputTestBase {
protected:
~StarnixTouchTest() override {
FX_CHECK(touch_injection_request_count() > 0) << "injection expected but didn't happen.";
}
// To satisfy ::testing::Test
void SetUp() override {
ui_testing::PortableUITest::SetUp();
FX_LOGS(INFO) << "Registering input injection device";
RegisterTouchScreen();
}
// For use by test cases.
void InjectInput(TapLocation tap_location) {
switch (tap_location) {
case TapLocation::kTopLeft:
InjectTap(display_width() / 4, display_height() / 4);
break;
case TapLocation::kBottomRight:
InjectTap(3 * display_width() / 4, 3 * display_height() / 4);
break;
}
}
// Reads sequences of touch events from `input_dump.cc`, via `out_socket`
// until we get num_expected events.
//
// Because of the variable amount of packets read at a time, we may create
// varying amounts of TouchEvents from a single call to GetEvDevPackets.
// Therefore we use the running size of the final result to determine whether
// to read more packets.
std::vector<TouchEvent> GetTouchEventSequenceOfLen(zx::socket& out_socket, size_t num_expected) {
std::vector<TouchEvent> result;
while (result.size() < num_expected) {
std::vector<EvDevPacket> pkts = GetEvDevPackets(out_socket);
for (EvDevPacket pkt : pkts) {
if (pkt.type == EV_SYN) {
continue;
}
if (pkt.type == EV_KEY) {
if (pkt.code != BTN_TOUCH) {
FX_LOGS(FATAL) << "unexpected key event code in touch event seq, code=" << pkt.code;
}
result.push_back(TouchEvent{.has_btn_touch = true, .btn_touch = pkt.value});
continue;
}
if (pkt.type != EV_ABS) {
FX_LOGS(FATAL) << "unexpected event type in touch event seq, type=" << pkt.type;
}
switch (pkt.code) {
case ABS_MT_SLOT:
result.push_back(
TouchEvent{.phase = fuchsia_ui_pointer::EventPhase::kChange, .slot_id = pkt.value});
break;
case ABS_MT_TRACKING_ID:
if (result.empty()) {
FX_LOGS(FATAL) << "receive ABS_MT_TRACKING_ID out of slot";
}
if (pkt.value == -1) {
result[result.size() - 1].phase = fuchsia_ui_pointer::EventPhase::kRemove;
} else {
result[result.size() - 1].phase = fuchsia_ui_pointer::EventPhase::kAdd;
result[result.size() - 1].pointer_id = pkt.value;
}
break;
case ABS_MT_POSITION_X:
if (result.empty()) {
FX_LOGS(FATAL) << "receive ABS_MT_POSITION_X out of slot";
}
result[result.size() - 1].local_x = static_cast<float>(pkt.value);
break;
case ABS_MT_POSITION_Y:
if (result.empty()) {
FX_LOGS(FATAL) << "receive ABS_MT_POSITION_X out of slot";
}
result[result.size() - 1].local_y = static_cast<float>(pkt.value);
break;
default:
FX_LOGS(FATAL) << "unexpected event code in touch event seq, code=" << pkt.code;
}
}
FX_LOGS(INFO) << "Read " << result.size() << " events of " << num_expected;
}
EXPECT_EQ(result.size(), num_expected);
return result;
}
void InitTouchInputRelay(zx::socket& in_socket, zx::socket& out_socket) {
// Wait for `input_dump` to start.
WaitForMessageFromInputDump(out_socket, relay_api::kWaitForStdinMessage);
// Inform `input_dump` to run the touch_relay.
std::stringstream ss;
ss << relay_api::kDeviceDelimiter << " " << relay_api::kTouchDev;
WriteMessageToSocket(in_socket, ss.str());
}
};
// TODO: https://fxbug.dev/42082519 - Test for DPR=2.0, too.
TEST_F(StarnixTouchTest, Tap) {
auto [in_socket, out_socket] = LaunchDumper();
// Wait until #launch_input is presented before injecting input.
WaitForViewPresentation();
// Start `input_dump` for a touch device.
InitTouchInputRelay(in_socket, out_socket);
// `input_dump` is ready to receive events.
WaitForMessageFromInputDump(out_socket, relay_api::kWaitForStdinMessage);
std::stringstream ss;
// This test expects 2 down - up event sequences.
ss << relay_api::kEventCmd << " " << relay_api::kDownUpNumPackets * 2;
WriteMessageToSocket(in_socket, ss.str());
// Wait for `input_dump` to ready for event injection.
WaitForMessageFromInputDump(out_socket, relay_api::kReadyMessage);
// Top-left.
InjectInput(TapLocation::kTopLeft);
{
auto events = GetTouchEventSequenceOfLen(out_socket, kDownUpNumEvents);
ExpectLocationPhaseAndSlot(events[0], static_cast<float>(display_width()) / 4.f,
static_cast<float>(display_height()) / 4.f,
fuchsia_ui_pointer::EventPhase::kAdd, 0);
ExpectBtnTouch(events[1], 1);
ExpectLocationPhaseAndSlot(events[2], 0.0, 0.0, fuchsia_ui_pointer::EventPhase::kRemove, 0);
ExpectBtnTouch(events[3], 0);
}
// Bottom-right.
InjectInput(TapLocation::kBottomRight);
{
auto events = GetTouchEventSequenceOfLen(out_socket, kDownUpNumEvents);
ExpectLocationPhaseAndSlot(events[0], 3 * static_cast<float>(display_width()) / 4.f,
3 * static_cast<float>(display_height()) / 4.f,
fuchsia_ui_pointer::EventPhase::kAdd, 0);
ExpectBtnTouch(events[1], 1);
ExpectLocationPhaseAndSlot(events[2], 0.0, 0.0, fuchsia_ui_pointer::EventPhase::kRemove, 0);
ExpectBtnTouch(events[3], 0);
}
WaitForMessageFromInputDump(out_socket, relay_api::kWaitForStdinMessage);
WriteMessageToSocket(in_socket, "quit");
}
// EventsDuringFileCloseAreIgnored ensure event reader does not get events before open.
TEST_F(StarnixTouchTest, EventsDuringFileCloseAreIgnored) {
auto [in_socket, out_socket] = LaunchDumper();
// Wait until #launch_input is presented before injecting input.
WaitForViewPresentation();
// Start `input_dump` for a touch device.
InitTouchInputRelay(in_socket, out_socket);
// `input_dump` is ready to receive events.
WaitForMessageFromInputDump(out_socket, relay_api::kWaitForStdinMessage);
std::stringstream ss;
ss << relay_api::kEventCmd << " " << relay_api::kDownUpNumPackets;
WriteMessageToSocket(in_socket, ss.str());
WaitForMessageFromInputDump(out_socket, relay_api::kReadyMessage);
// Send 1 tap to top left.
InjectInput(TapLocation::kTopLeft);
{
auto events = GetTouchEventSequenceOfLen(out_socket, kDownUpNumEvents);
ExpectLocationPhaseAndSlot(events[0], static_cast<float>(display_width()) / 4.f,
static_cast<float>(display_height()) / 4.f,
fuchsia_ui_pointer::EventPhase::kAdd, 0);
ExpectBtnTouch(events[1], 1);
ExpectLocationPhaseAndSlot(events[2], 0.0, 0.0, fuchsia_ui_pointer::EventPhase::kRemove, 0);
ExpectBtnTouch(events[3], 0);
}
FX_LOGS(INFO) << "device file closed";
// Now the file is closed. Send 1 tap to top left. input_dump should not receive this event
// sequence.
InjectInput(TapLocation::kTopLeft);
// TODO(https://fxbug.dev/375021518): Here should block on a state instead of timeout to ensure
// events are reached to Starnix before open file below.
// It is ok if this test is flaky when the tap top left reach to starnix after "device file
// opened". It just means this timeout is not enough.
RunLoopWithTimeout(zx::sec(1));
WaitForMessageFromInputDump(out_socket, relay_api::kWaitForStdinMessage);
WriteMessageToSocket(in_socket, ss.str());
FX_LOGS(INFO) << "device file opened";
// Wait for `input_dump` to ready for event injection.
WaitForMessageFromInputDump(out_socket, relay_api::kReadyMessage);
// Send 1 tap to bottom right. input_dump should receive this event sequence.
InjectInput(TapLocation::kBottomRight);
{
auto events = GetTouchEventSequenceOfLen(out_socket, kDownUpNumEvents);
ExpectLocationPhaseAndSlot(events[0], 3 * static_cast<float>(display_width()) / 4.f,
3 * static_cast<float>(display_height()) / 4.f,
fuchsia_ui_pointer::EventPhase::kAdd, 0);
ExpectBtnTouch(events[1], 1);
ExpectLocationPhaseAndSlot(events[2], 0.0, 0.0, fuchsia_ui_pointer::EventPhase::kRemove, 0);
ExpectBtnTouch(events[3], 0);
}
WaitForMessageFromInputDump(out_socket, relay_api::kWaitForStdinMessage);
WriteMessageToSocket(in_socket, "quit");
}
// OpenFileDuringEventSequenceReceivesPartialSequence tests event delivery when the device file is
// opened in the middle of an event sequence. It verifies that only events generated after the file
// is opened are recorded in input_dump.
TEST_F(StarnixTouchTest, OpenFileDuringEventSequenceReceivesPartialSequence) {
auto [in_socket, out_socket] = LaunchDumper();
// Wait until #launch_input is presented before injecting input.
WaitForViewPresentation();
// Start `input_dump` for a touch device.
InitTouchInputRelay(in_socket, out_socket);
// `input_dump` is ready to receive events.
WaitForMessageFromInputDump(out_socket, relay_api::kWaitForStdinMessage);
std::stringstream ss;
ss << relay_api::kEventCmd << " " << relay_api::kDownUpNumPackets;
WriteMessageToSocket(in_socket, ss.str());
WaitForMessageFromInputDump(out_socket, relay_api::kReadyMessage);
// Send 1 tap to top left.
InjectInput(TapLocation::kTopLeft);
{
auto events = GetTouchEventSequenceOfLen(out_socket, kDownUpNumEvents);
ExpectLocationPhaseAndSlot(events[0], static_cast<float>(display_width()) / 4.f,
static_cast<float>(display_height()) / 4.f,
fuchsia_ui_pointer::EventPhase::kAdd, 0);
ExpectBtnTouch(events[1], 1);
ExpectLocationPhaseAndSlot(events[2], 0.0, 0.0, fuchsia_ui_pointer::EventPhase::kRemove, 0);
ExpectBtnTouch(events[3], 0);
}
FX_LOGS(INFO) << "device file closed";
// Now the file is closed. Send 1 tap to top left. input_dump should not receive this down event.
fuchsia_input_report::TouchInputReport down;
down.contacts({{fuchsia_input_report::ContactInputReport{
{
.contact_id = 1,
.position_x = display_width() / 4,
.position_y = display_height() / 4,
},
}}});
InjectTouchEvent(down);
// TODO(https://fxbug.dev/375021518): Here should block on a state instead of timeout to ensure
// events are reached to Starnix before open file below.
// It is ok if this test is flaky when the tap top left reach to starnix after "device file
// opened". It just means this timeout is not enough.
RunLoopWithTimeout(zx::sec(1));
WaitForMessageFromInputDump(out_socket, relay_api::kWaitForStdinMessage);
// Inject touch move and up.
ss.str(""); // clear the stringstream
ss << relay_api::kEventCmd << " " << relay_api::kMoveNumPackets + relay_api::kUpNumPackets;
WriteMessageToSocket(in_socket, ss.str());
FX_LOGS(INFO) << "device file opened";
// Wait for `input_dump` to ready for event injection.
WaitForMessageFromInputDump(out_socket, relay_api::kReadyMessage);
fuchsia_input_report::TouchInputReport move;
move.contacts({{fuchsia_input_report::ContactInputReport{
{
.contact_id = 1,
.position_x = display_width() / 4 * 3,
.position_y = display_height() / 4 * 3,
},
}}});
InjectTouchEvent(move);
fuchsia_input_report::TouchInputReport up;
up.contacts({{}});
InjectTouchEvent(up);
{
auto events = GetTouchEventSequenceOfLen(out_socket, kMoveUpNumEvents);
ExpectLocationPhaseAndSlot(events[0], 3 * static_cast<float>(display_width()) / 4.f,
3 * static_cast<float>(display_height()) / 4.f,
fuchsia_ui_pointer::EventPhase::kChange, 0);
ExpectLocationPhaseAndSlot(events[1], 0.0, 0.0, fuchsia_ui_pointer::EventPhase::kRemove, 0);
ExpectBtnTouch(events[2], 0);
}
WaitForMessageFromInputDump(out_socket, relay_api::kWaitForStdinMessage);
WriteMessageToSocket(in_socket, "quit");
}
} // namespace