blob: e6c8e9ed10af456a2f200cf44ec2b1d90fe987fe [file]
// Copyright 2026 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 "src/ui/scenic/lib/input/touch_source_v2.h"
#include <fidl/fuchsia.ui.pointer/cpp/fidl.h>
#include <lib/async-testing/test_loop.h>
#include <lib/syslog/cpp/macros.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "src/lib/testing/loop_fixture/test_loop_fixture.h"
namespace input::test {
using scenic_impl::input::GestureContenderInspector;
using scenic_impl::input::GestureResponse;
using scenic_impl::input::InternalTouchEvent;
using scenic_impl::input::Phase;
using scenic_impl::input::StreamId;
using scenic_impl::input::TouchSourceV2;
constexpr zx_koid_t kViewRefKoid = 25;
constexpr StreamId kStreamId = 1;
constexpr uint32_t kDeviceId = 2;
constexpr uint32_t kPointerId = 3;
const view_tree::Snapshot kSnapshot{};
namespace {
InternalTouchEvent CreateTouchEvent(Phase phase) {
InternalTouchEvent event;
event.device_id = kDeviceId;
event.pointer_id = kPointerId;
event.phase = phase;
event.viewport = {
.receiver_from_viewport_transform = std::array<float, 9>({1, 0, 0, 0, 1, 0, 0, 0, 1}),
};
return event;
}
class TestTouchSourceV2Client : public fidl::AsyncEventHandler<fuchsia_ui_pointer::TouchSourceV2> {
public:
void OnTouchEvents(
fidl::Event<fuchsia_ui_pointer::TouchSourceV2::OnTouchEvents>& event) override {
received_events_.insert(received_events_.end(), std::make_move_iterator(event.events().begin()),
std::make_move_iterator(event.events().end()));
last_event_stamp_ = event.last_event_stamp();
}
void on_fidl_error(fidl::UnbindInfo info) override { unbind_info_ = info; }
void handle_unknown_event(
fidl::UnknownEventMetadata<fuchsia_ui_pointer::TouchSourceV2> metadata) override {}
std::vector<fuchsia_ui_pointer::TouchEvent> received_events_;
uint64_t last_event_stamp_ = 0;
std::optional<fidl::UnbindInfo> unbind_info_;
};
} // namespace
class TouchSourceV2Test : public gtest::TestLoopFixture {
protected:
void SetUp() override {
auto [client_end, server_end] = fidl::Endpoints<fuchsia_ui_pointer::TouchSourceV2>::Create();
touch_source_.emplace(
dispatcher(), kViewRefKoid, std::move(server_end),
/*respond*/
[this](StreamId stream_id, const std::vector<GestureResponse>& responses) {
std::copy(responses.begin(), responses.end(),
std::back_inserter(received_responses_[stream_id]));
},
/*error_handler*/ [this] { internal_error_handler_fired_ = true; }, inspector_);
client_.emplace(std::move(client_end), dispatcher(), &event_handler_);
}
bool internal_error_handler_fired_ = false;
std::unordered_map<StreamId, std::vector<GestureResponse>> received_responses_;
GestureContenderInspector inspector_{inspect::Node()};
TestTouchSourceV2Client event_handler_;
std::optional<fidl::Client<fuchsia_ui_pointer::TouchSourceV2>> client_;
std::optional<TouchSourceV2> touch_source_;
};
TEST_F(TouchSourceV2Test, UpdateStream_AlwaysRespondsYes) {
InternalTouchEvent add_event = CreateTouchEvent(Phase::kAdd);
touch_source_->UpdateStream(kSnapshot, kStreamId, std::move(add_event),
/*is_end_of_stream*/ false, {});
EXPECT_THAT(received_responses_[kStreamId], testing::ElementsAre(GestureResponse::kYes));
}
TEST_F(TouchSourceV2Test, UpdateStream_EndOfStream_RespondsYes) {
InternalTouchEvent add_event = CreateTouchEvent(Phase::kAdd);
touch_source_->UpdateStream(kSnapshot, kStreamId, std::move(add_event),
/*is_end_of_stream*/ false, {});
received_responses_.clear();
InternalTouchEvent remove_event = CreateTouchEvent(Phase::kRemove);
touch_source_->UpdateStream(kSnapshot, kStreamId, std::move(remove_event),
/*is_end_of_stream*/ true, {});
EXPECT_THAT(received_responses_[kStreamId], testing::ElementsAre(GestureResponse::kYes));
}
TEST_F(TouchSourceV2Test, PushEventsAndAck) {
InternalTouchEvent add_event = CreateTouchEvent(Phase::kAdd);
touch_source_->UpdateStream(kSnapshot, kStreamId, std::move(add_event),
/*is_end_of_stream*/ false, {});
RunLoopUntilIdle();
EXPECT_EQ(event_handler_.received_events_.size(), 1u);
EXPECT_EQ(event_handler_.last_event_stamp_, 1u);
// Send ACK to server.
auto ack_result = (*client_)->AcknowledgeEvents({1});
EXPECT_TRUE(ack_result.is_ok());
RunLoopUntilIdle();
EXPECT_FALSE(internal_error_handler_fired_);
}
TEST_F(TouchSourceV2Test, FlowControl_BuffersAndFlushesOnAck) {
// Push kMaxUnacknowledgedEvents (150) events.
for (uint64_t i = 1; i <= TouchSourceV2::kMaxUnacknowledgedEvents; ++i) {
Phase phase = (i == 1) ? Phase::kAdd : Phase::kChange;
InternalTouchEvent event = CreateTouchEvent(phase);
touch_source_->UpdateStream(kSnapshot, kStreamId, std::move(event), /*is_end_of_stream*/ false,
{});
}
RunLoopUntilIdle();
EXPECT_EQ(event_handler_.received_events_.size(), TouchSourceV2::kMaxUnacknowledgedEvents);
EXPECT_EQ(event_handler_.last_event_stamp_, TouchSourceV2::kMaxUnacknowledgedEvents);
// Push 151st event. Limit reached (150 unacked), so it should be buffered.
InternalTouchEvent extra_event = CreateTouchEvent(Phase::kChange);
touch_source_->UpdateStream(kSnapshot, kStreamId, std::move(extra_event),
/*is_end_of_stream*/ false, {});
RunLoopUntilIdle();
// No new events received by client yet.
EXPECT_EQ(event_handler_.received_events_.size(), TouchSourceV2::kMaxUnacknowledgedEvents);
// Acknowledge the 150 events.
auto ack_result = (*client_)->AcknowledgeEvents({TouchSourceV2::kMaxUnacknowledgedEvents});
EXPECT_TRUE(ack_result.is_ok());
RunLoopUntilIdle();
// Now the buffered 151st event is sent.
EXPECT_EQ(event_handler_.received_events_.size(), TouchSourceV2::kMaxUnacknowledgedEvents + 1);
EXPECT_EQ(event_handler_.last_event_stamp_, TouchSourceV2::kMaxUnacknowledgedEvents + 1);
}
TEST_F(TouchSourceV2Test, FlowControl_PartialBatchAck) {
// Push 10 events (all sent in 1 batch, stamps 1..10).
for (uint64_t i = 1; i <= 10; ++i) {
Phase phase = (i == 1) ? Phase::kAdd : Phase::kChange;
InternalTouchEvent event = CreateTouchEvent(phase);
touch_source_->UpdateStream(kSnapshot, kStreamId, std::move(event), /*is_end_of_stream*/ false,
{});
}
RunLoopUntilIdle();
EXPECT_EQ(event_handler_.received_events_.size(), 10u);
EXPECT_EQ(event_handler_.last_event_stamp_, 10u);
// Partially acknowledge stamp 5.
auto ack_result = (*client_)->AcknowledgeEvents({5});
EXPECT_TRUE(ack_result.is_ok());
RunLoopUntilIdle();
EXPECT_FALSE(internal_error_handler_fired_);
// Fully acknowledge stamp 10.
ack_result = (*client_)->AcknowledgeEvents({10});
EXPECT_TRUE(ack_result.is_ok());
RunLoopUntilIdle();
EXPECT_FALSE(internal_error_handler_fired_);
}
TEST_F(TouchSourceV2Test, InvalidAck_ClosesChannel) {
InternalTouchEvent add_event = CreateTouchEvent(Phase::kAdd);
touch_source_->UpdateStream(kSnapshot, kStreamId, std::move(add_event),
/*is_end_of_stream*/ false, {});
RunLoopUntilIdle();
// Acknowledge stamp 999 (which hasn't been sent yet).
auto ack_result = (*client_)->AcknowledgeEvents({999});
EXPECT_TRUE(ack_result.is_ok());
RunLoopUntilIdle();
EXPECT_TRUE(internal_error_handler_fired_);
}
TEST_F(TouchSourceV2Test, DuplicateOrEarlierAck_ClosesChannel) {
for (uint64_t i = 1; i <= 5; ++i) {
Phase phase = (i == 1) ? Phase::kAdd : Phase::kChange;
InternalTouchEvent event = CreateTouchEvent(phase);
touch_source_->UpdateStream(kSnapshot, kStreamId, std::move(event), /*is_end_of_stream*/ false,
{});
}
RunLoopUntilIdle();
EXPECT_EQ(event_handler_.received_events_.size(), 5u);
EXPECT_EQ(event_handler_.last_event_stamp_, 5u);
// Send ACK for stamp 5.
auto ack_result = (*client_)->AcknowledgeEvents({5});
EXPECT_TRUE(ack_result.is_ok());
RunLoopUntilIdle();
EXPECT_FALSE(internal_error_handler_fired_);
// Send duplicate ACK for stamp 5.
ack_result = (*client_)->AcknowledgeEvents({5});
EXPECT_TRUE(ack_result.is_ok());
RunLoopUntilIdle();
EXPECT_TRUE(internal_error_handler_fired_);
}
TEST_F(TouchSourceV2Test, EarlierAck_ClosesChannel) {
for (uint64_t i = 1; i <= 5; ++i) {
Phase phase = (i == 1) ? Phase::kAdd : Phase::kChange;
InternalTouchEvent event = CreateTouchEvent(phase);
touch_source_->UpdateStream(kSnapshot, kStreamId, std::move(event), /*is_end_of_stream*/ false,
{});
}
RunLoopUntilIdle();
EXPECT_EQ(event_handler_.received_events_.size(), 5u);
EXPECT_EQ(event_handler_.last_event_stamp_, 5u);
// Send ACK for stamp 4.
auto ack_result = (*client_)->AcknowledgeEvents({4});
EXPECT_TRUE(ack_result.is_ok());
RunLoopUntilIdle();
EXPECT_FALSE(internal_error_handler_fired_);
// Send earlier ACK for stamp 3.
ack_result = (*client_)->AcknowledgeEvents({3});
EXPECT_TRUE(ack_result.is_ok());
RunLoopUntilIdle();
EXPECT_TRUE(internal_error_handler_fired_);
}
TEST_F(TouchSourceV2Test, EndContest_AwardedWin_EmitsGranted) {
InternalTouchEvent add_event = CreateTouchEvent(Phase::kAdd);
touch_source_->UpdateStream(kSnapshot, kStreamId, std::move(add_event),
/*is_end_of_stream*/ false, {});
RunLoopUntilIdle();
ASSERT_EQ(event_handler_.received_events_.size(), 1u);
EXPECT_FALSE(event_handler_.received_events_[0].interaction_result().has_value());
touch_source_->EndContest(kStreamId, /*awarded_win=*/true);
RunLoopUntilIdle();
ASSERT_EQ(event_handler_.received_events_.size(), 2u);
ASSERT_TRUE(event_handler_.received_events_[1].interaction_result().has_value());
EXPECT_EQ(event_handler_.received_events_[1].interaction_result()->status(),
fuchsia_ui_pointer::TouchInteractionStatus::kGranted);
}
TEST_F(TouchSourceV2Test, EndContest_Denied_EmitsDeniedAndCleansUp) {
InternalTouchEvent add_event = CreateTouchEvent(Phase::kAdd);
touch_source_->UpdateStream(kSnapshot, kStreamId, std::move(add_event),
/*is_end_of_stream*/ false, {});
RunLoopUntilIdle();
ASSERT_EQ(event_handler_.received_events_.size(), 1u);
EXPECT_FALSE(event_handler_.received_events_[0].interaction_result().has_value());
touch_source_->EndContest(kStreamId, /*awarded_win=*/false);
RunLoopUntilIdle();
ASSERT_EQ(event_handler_.received_events_.size(), 2u);
ASSERT_TRUE(event_handler_.received_events_[1].interaction_result().has_value());
EXPECT_EQ(event_handler_.received_events_[1].interaction_result()->status(),
fuchsia_ui_pointer::TouchInteractionStatus::kDenied);
}
TEST_F(TouchSourceV2Test, EndContest_WinBeforeFirstUpdateStream) {
touch_source_->EndContest(kStreamId, /*awarded_win=*/true);
InternalTouchEvent add_event = CreateTouchEvent(Phase::kAdd);
touch_source_->UpdateStream(kSnapshot, kStreamId, std::move(add_event),
/*is_end_of_stream*/ false, {});
RunLoopUntilIdle();
ASSERT_EQ(event_handler_.received_events_.size(), 1u);
ASSERT_TRUE(event_handler_.received_events_[0].interaction_result().has_value());
EXPECT_EQ(event_handler_.received_events_[0].interaction_result()->status(),
fuchsia_ui_pointer::TouchInteractionStatus::kGranted);
}
TEST_F(TouchSourceV2Test, FlowControl_BufferOverflow_ClosesChannel) {
// Push kMaxUnacknowledgedEvents (150) events to fill available credit.
for (uint64_t i = 1; i <= TouchSourceV2::kMaxUnacknowledgedEvents; ++i) {
Phase phase = (i == 1) ? Phase::kAdd : Phase::kChange;
InternalTouchEvent event = CreateTouchEvent(phase);
touch_source_->UpdateStream(kSnapshot, kStreamId, std::move(event), /*is_end_of_stream*/ false,
{});
}
RunLoopUntilIdle();
EXPECT_EQ(event_handler_.received_events_.size(), TouchSourceV2::kMaxUnacknowledgedEvents);
EXPECT_FALSE(internal_error_handler_fired_);
// Now push kMaxBufferedEvents (1000) more events while unacknowledged.
// Channel should remain open.
for (size_t i = 0; i < TouchSourceV2::kMaxBufferedEvents; ++i) {
InternalTouchEvent event = CreateTouchEvent(Phase::kChange);
touch_source_->UpdateStream(kSnapshot, kStreamId, std::move(event), /*is_end_of_stream*/ false,
{});
}
RunLoopUntilIdle();
EXPECT_FALSE(internal_error_handler_fired_);
// Push 1 more event exceeding the buffer capacity. Channel should close.
InternalTouchEvent event = CreateTouchEvent(Phase::kChange);
touch_source_->UpdateStream(kSnapshot, kStreamId, std::move(event), /*is_end_of_stream*/ false,
{});
RunLoopUntilIdle();
EXPECT_TRUE(internal_error_handler_fired_);
}
TEST_F(TouchSourceV2Test, TouchDeviceInfo_SentOncePerDevice) {
constexpr uint32_t kDevice1 = 1;
constexpr uint32_t kDevice2 = 2;
// Stream 1 from Device 1 (ADD)
InternalTouchEvent event1 = CreateTouchEvent(Phase::kAdd);
event1.device_id = kDevice1;
touch_source_->UpdateStream(kSnapshot, 1, std::move(event1), /*is_end_of_stream*/ false, {});
// Stream 1 from Device 1 (CHANGE)
InternalTouchEvent event2 = CreateTouchEvent(Phase::kChange);
event2.device_id = kDevice1;
touch_source_->UpdateStream(kSnapshot, 1, std::move(event2), /*is_end_of_stream*/ false, {});
// Stream 2 from Device 2 (ADD)
InternalTouchEvent event3 = CreateTouchEvent(Phase::kAdd);
event3.device_id = kDevice2;
touch_source_->UpdateStream(kSnapshot, 2, std::move(event3), /*is_end_of_stream*/ false, {});
RunLoopUntilIdle();
ASSERT_EQ(event_handler_.received_events_.size(), 3u);
// Event 1 has device_info for Device 1
ASSERT_TRUE(event_handler_.received_events_[0].device_info().has_value());
EXPECT_EQ(event_handler_.received_events_[0].device_info()->id(), kDevice1);
// Event 2 does NOT have device_info
EXPECT_FALSE(event_handler_.received_events_[1].device_info().has_value());
// Event 3 has device_info for Device 2
ASSERT_TRUE(event_handler_.received_events_[2].device_info().has_value());
EXPECT_EQ(event_handler_.received_events_[2].device_info()->id(), kDevice2);
}
TEST_F(TouchSourceV2Test, ViewParameters_DeliveredOnFirstEventAndOnBoundsChange) {
view_tree::BoundingBox bounds1{.min = {0, 0}, .max = {100, 100}};
view_tree::BoundingBox bounds2{.min = {0, 0}, .max = {200, 200}};
// Event 1: First event should include view_parameters.
InternalTouchEvent event1 = CreateTouchEvent(Phase::kAdd);
touch_source_->UpdateStream(kSnapshot, kStreamId, std::move(event1), /*is_end_of_stream*/ false,
bounds1);
// Event 2: Same bounds, should NOT include view_parameters.
InternalTouchEvent event2 = CreateTouchEvent(Phase::kChange);
touch_source_->UpdateStream(kSnapshot, kStreamId, std::move(event2), /*is_end_of_stream*/ false,
bounds1);
// Event 3: Changed bounds, should include view_parameters.
InternalTouchEvent event3 = CreateTouchEvent(Phase::kChange);
touch_source_->UpdateStream(kSnapshot, kStreamId, std::move(event3), /*is_end_of_stream*/ false,
bounds2);
RunLoopUntilIdle();
ASSERT_EQ(event_handler_.received_events_.size(), 3u);
EXPECT_TRUE(event_handler_.received_events_[0].view_parameters().has_value());
EXPECT_FALSE(event_handler_.received_events_[1].view_parameters().has_value());
EXPECT_TRUE(event_handler_.received_events_[2].view_parameters().has_value());
}
TEST_F(TouchSourceV2Test, WakeLease_Forwarded) {
zx::eventpair lease_token, peer_token;
ASSERT_EQ(zx::eventpair::create(0, &lease_token, &peer_token), ZX_OK);
InternalTouchEvent event = CreateTouchEvent(Phase::kAdd);
event.wake_lease = std::move(lease_token);
touch_source_->UpdateStream(kSnapshot, kStreamId, std::move(event), /*is_end_of_stream*/ false,
{});
RunLoopUntilIdle();
ASSERT_EQ(event_handler_.received_events_.size(), 1u);
EXPECT_TRUE(event_handler_.received_events_[0].wake_lease().has_value());
EXPECT_TRUE(event_handler_.received_events_[0].wake_lease()->is_valid());
}
TEST_F(TouchSourceV2Test, EventsAfterClose_DoNotCrashOrDoubleClose) {
// Trigger close with invalid ack.
auto ack_result = (*client_)->AcknowledgeEvents({999});
EXPECT_TRUE(ack_result.is_ok());
RunLoopUntilIdle();
EXPECT_TRUE(internal_error_handler_fired_);
received_responses_.clear();
// Now push many events to verify no double close / crash occurs.
for (size_t i = 0; i < TouchSourceV2::kMaxBufferedEvents + 10; ++i) {
InternalTouchEvent event = CreateTouchEvent(Phase::kChange);
touch_source_->UpdateStream(kSnapshot, kStreamId, std::move(event), /*is_end_of_stream*/ false,
{});
}
RunLoopUntilIdle();
// Should have responded kNo for each event to satisfy Gesture Arena contract.
EXPECT_EQ(received_responses_[kStreamId].size(), TouchSourceV2::kMaxBufferedEvents + 10);
for (const auto& response : received_responses_[kStreamId]) {
EXPECT_EQ(response, GestureResponse::kNo);
}
}
TEST_F(TouchSourceV2Test, SynchronousLoseContestDuringUpdateStream) {
auto [client_end, server_end] = fidl::Endpoints<fuchsia_ui_pointer::TouchSourceV2>::Create();
TestTouchSourceV2Client client_event_handler;
fidl::Client<fuchsia_ui_pointer::TouchSourceV2> client(std::move(client_end), dispatcher(),
&client_event_handler);
std::optional<TouchSourceV2> ts;
ts.emplace(
dispatcher(), kViewRefKoid, std::move(server_end),
/*respond*/
[&ts](StreamId stream_id, const std::vector<GestureResponse>& responses) {
// Synchronously simulate losing the contest during respond callback.
ts->EndContest(stream_id, /*awarded_win=*/false);
},
/*error_handler*/ [] {}, inspector_);
InternalTouchEvent add_event = CreateTouchEvent(Phase::kAdd);
ts->UpdateStream(kSnapshot, kStreamId, std::move(add_event), /*is_end_of_stream=*/false, {});
RunLoopUntilIdle();
// The client should receive the ADD event followed by the DENIED interaction result.
ASSERT_EQ(client_event_handler.received_events_.size(), 2u);
EXPECT_TRUE(client_event_handler.received_events_[0].pointer_sample().has_value());
EXPECT_EQ(client_event_handler.received_events_[0].pointer_sample()->phase(),
fuchsia_ui_pointer::EventPhase::kAdd);
ASSERT_TRUE(client_event_handler.received_events_[1].interaction_result().has_value());
EXPECT_EQ(client_event_handler.received_events_[1].interaction_result()->status(),
fuchsia_ui_pointer::TouchInteractionStatus::kDenied);
}
TEST_F(TouchSourceV2Test, SynchronousLoseContestDuringChangeUpdateStream) {
auto [client_end, server_end] = fidl::Endpoints<fuchsia_ui_pointer::TouchSourceV2>::Create();
TestTouchSourceV2Client client_event_handler;
fidl::Client<fuchsia_ui_pointer::TouchSourceV2> client(std::move(client_end), dispatcher(),
&client_event_handler);
bool should_lose_on_change = false;
std::optional<TouchSourceV2> ts;
ts.emplace(
dispatcher(), kViewRefKoid, std::move(server_end),
/*respond*/
[&ts, &should_lose_on_change](StreamId stream_id,
const std::vector<GestureResponse>& responses) {
if (should_lose_on_change) {
ts->EndContest(stream_id, /*awarded_win=*/false);
}
},
/*error_handler*/ [] {}, inspector_);
InternalTouchEvent add_event = CreateTouchEvent(Phase::kAdd);
ts->UpdateStream(kSnapshot, kStreamId, std::move(add_event), /*is_end_of_stream=*/false, {});
RunLoopUntilIdle();
ASSERT_EQ(client_event_handler.received_events_.size(), 1u);
should_lose_on_change = true;
InternalTouchEvent change_event = CreateTouchEvent(Phase::kChange);
ts->UpdateStream(kSnapshot, kStreamId, std::move(change_event), /*is_end_of_stream=*/false, {});
RunLoopUntilIdle();
// The client should receive the CHANGE event followed by the DENIED interaction result.
ASSERT_EQ(client_event_handler.received_events_.size(), 3u);
EXPECT_TRUE(client_event_handler.received_events_[1].pointer_sample().has_value());
EXPECT_EQ(client_event_handler.received_events_[1].pointer_sample()->phase(),
fuchsia_ui_pointer::EventPhase::kChange);
ASSERT_TRUE(client_event_handler.received_events_[2].interaction_result().has_value());
EXPECT_EQ(client_event_handler.received_events_[2].interaction_result()->status(),
fuchsia_ui_pointer::TouchInteractionStatus::kDenied);
}
} // namespace input::test