blob: e70b899b63513e78871bffc2b9ce03fb7f421085 [file] [log] [blame]
// Copyright 2021 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.h"
#include <lib/async/cpp/time.h>
#include <lib/async/default.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/trace/event.h>
#include <unordered_map>
#include "src/lib/fxl/macros.h"
namespace scenic_impl::input {
namespace {
GestureResponse ConvertToGestureResponse(fuchsia::ui::pointer::TouchResponseType type) {
switch (type) {
case fuchsia::ui::pointer::TouchResponseType::YES:
return GestureResponse::kYes;
case fuchsia::ui::pointer::TouchResponseType::YES_PRIORITIZE:
return GestureResponse::kYesPrioritize;
case fuchsia::ui::pointer::TouchResponseType::NO:
return GestureResponse::kNo;
case fuchsia::ui::pointer::TouchResponseType::MAYBE:
return GestureResponse::kMaybe;
case fuchsia::ui::pointer::TouchResponseType::MAYBE_PRIORITIZE:
return GestureResponse::kMaybePrioritize;
case fuchsia::ui::pointer::TouchResponseType::MAYBE_SUPPRESS:
return GestureResponse::kMaybeSuppress;
case fuchsia::ui::pointer::TouchResponseType::MAYBE_PRIORITIZE_SUPPRESS:
return GestureResponse::kMaybePrioritizeSuppress;
case fuchsia::ui::pointer::TouchResponseType::HOLD:
return GestureResponse::kHold;
case fuchsia::ui::pointer::TouchResponseType::HOLD_SUPPRESS:
return GestureResponse::kHoldSuppress;
default:
return GestureResponse::kUndefined;
}
}
fuchsia::ui::pointer::EventPhase ConvertToEventPhase(Phase phase) {
switch (phase) {
case Phase::kAdd:
return fuchsia::ui::pointer::EventPhase::ADD;
case Phase::kChange:
return fuchsia::ui::pointer::EventPhase::CHANGE;
case Phase::kRemove:
return fuchsia::ui::pointer::EventPhase::REMOVE;
case Phase::kCancel:
return fuchsia::ui::pointer::EventPhase::CANCEL;
default:
// Never reached.
FX_CHECK(false) << "Unknown phase: " << phase;
return fuchsia::ui::pointer::EventPhase::CANCEL;
}
}
fuchsia::ui::pointer::TouchEvent NewTouchEvent(StreamId stream_id,
const InternalPointerEvent& event,
bool is_end_of_stream) {
fuchsia::ui::pointer::TouchEvent new_event;
new_event.set_timestamp(event.timestamp);
new_event.set_trace_flow_id(TRACE_NONCE());
{
fuchsia::ui::pointer::TouchPointerSample pointer;
pointer.set_phase(ConvertToEventPhase(event.phase));
pointer.set_position_in_viewport(
{event.position_in_viewport[0], event.position_in_viewport[1]});
pointer.set_interaction(fuchsia::ui::pointer::TouchInteractionId{
.device_id = event.device_id, .pointer_id = event.pointer_id, .interaction_id = stream_id});
new_event.set_pointer_sample(std::move(pointer));
}
return new_event;
}
fuchsia::ui::pointer::TouchEvent NewEndEvent(StreamId stream_id, uint32_t device_id,
uint32_t pointer_id, bool awarded_win) {
fuchsia::ui::pointer::TouchEvent new_event;
new_event.set_timestamp(async::Now(async_get_default_dispatcher()).get());
new_event.set_interaction_result(fuchsia::ui::pointer::TouchInteractionResult{
.interaction =
fuchsia::ui::pointer::TouchInteractionId{
.device_id = device_id, .pointer_id = pointer_id, .interaction_id = stream_id},
.status = awarded_win ? fuchsia::ui::pointer::TouchInteractionStatus::GRANTED
: fuchsia::ui::pointer::TouchInteractionStatus::DENIED});
return new_event;
}
void AddViewParametersToEvent(fuchsia::ui::pointer::TouchEvent& event, const Viewport& viewport) {
event.set_view_parameters(
fuchsia::ui::pointer::ViewParameters{
.view =
fuchsia::ui::pointer::Rectangle{
// TODO(fxbug.dev/73639): Add view bounds.
},
.viewport =
fuchsia::ui::pointer::Rectangle{
.min = {{viewport.extents.min[0], viewport.extents.min[1]}},
.max = {{viewport.extents.max[0], viewport.extents.max[1]}}},
.viewport_to_view_transform = {viewport.context_from_viewport_transform[0][0],
viewport.context_from_viewport_transform[0][1],
viewport.context_from_viewport_transform[0][2],
viewport.context_from_viewport_transform[1][0],
viewport.context_from_viewport_transform[1][1],
viewport.context_from_viewport_transform[1][2],
viewport.context_from_viewport_transform[2][0],
viewport.context_from_viewport_transform[2][1],
viewport.context_from_viewport_transform[2][2]}});
}
bool IsHold(GestureResponse response) {
switch (response) {
case GestureResponse::kHold:
case GestureResponse::kHoldSuppress:
return true;
default:
return false;
}
}
bool IsHold(fuchsia::ui::pointer::TouchResponseType response) {
switch (response) {
case fuchsia::ui::pointer::TouchResponseType::HOLD:
case fuchsia::ui::pointer::TouchResponseType::HOLD_SUPPRESS:
return true;
default:
return false;
}
}
} // namespace
TouchSource::TouchSource(fidl::InterfaceRequest<fuchsia::ui::pointer::TouchSource> event_provider,
fit::function<void(StreamId, const std::vector<GestureResponse>&)> respond,
fit::function<void()> error_handler)
: binding_(this, std::move(event_provider)),
respond_(std::move(respond)),
error_handler_(std::move(error_handler)) {
binding_.set_error_handler([this](zx_status_t) { error_handler_(); });
}
TouchSource::~TouchSource() {
// Cancel ongoing streams
for (const auto& [id, data] : ongoing_streams_) {
if (!data.was_won) {
respond_(id, {GestureResponse::kNo});
}
}
}
void TouchSource::UpdateStream(StreamId stream_id, const InternalPointerEvent& event,
bool is_end_of_stream) {
const bool is_new_stream = ongoing_streams_.count(stream_id) == 0;
FX_CHECK(is_new_stream == (event.phase == Phase::kAdd)) << "Stream must only start with ADD.";
FX_CHECK(is_end_of_stream == (event.phase == Phase::kRemove || event.phase == Phase::kCancel));
if (is_new_stream) {
ongoing_streams_.try_emplace(
stream_id, StreamData{.device_id = event.device_id, .pointer_id = event.pointer_id});
}
auto& stream = ongoing_streams_.at(stream_id);
// Filter legacy events.
// TODO(fxbug.dev/53316): Remove when we no longer need to filter events.
++stream.num_pointer_events;
if (event.phase == Phase::kDown || event.phase == Phase::kUp) {
FX_DCHECK(!is_end_of_stream);
FX_DCHECK(stream.num_pointer_events > 1);
stream.filtered_events.emplace(stream.num_pointer_events);
return;
}
auto out_event = NewTouchEvent(stream_id, event, is_end_of_stream);
if (is_new_stream) {
fuchsia::ui::pointer::TouchDeviceInfo device_info;
device_info.set_id(event.device_id);
out_event.set_device_info(std::move(device_info));
}
stream.stream_has_ended = is_end_of_stream;
const auto viewport = event.viewport;
if (current_viewport_ != viewport || is_first_event_) {
is_first_event_ = false;
current_viewport_ = viewport;
AddViewParametersToEvent(out_event, current_viewport_);
}
pending_events_.push({.stream_id = stream_id, .event = std::move(out_event)});
SendPendingIfWaiting();
}
void TouchSource::EndContest(StreamId stream_id, bool awarded_win) {
FX_DCHECK(ongoing_streams_.count(stream_id) != 0);
auto& stream = ongoing_streams_[stream_id];
stream.was_won = awarded_win;
pending_events_.push(
{.stream_id = stream_id,
.event = NewEndEvent(stream_id, stream.device_id, stream.pointer_id, awarded_win)});
SendPendingIfWaiting();
if (!awarded_win) {
ongoing_streams_.erase(stream_id);
}
}
zx_status_t TouchSource::ValidateResponses(
const std::vector<fuchsia::ui::pointer::TouchResponse>& responses,
const std::vector<ReturnTicket>& return_tickets, bool have_pending_callback) {
if (have_pending_callback) {
FX_LOGS(ERROR) << "TouchSource: Client called Watch twice without waiting for response.";
return ZX_ERR_BAD_STATE;
}
if (return_tickets.size() != responses.size()) {
FX_LOGS(ERROR)
<< "TouchSource: Client called Watch with the wrong number of responses. Expected: "
<< return_tickets.size() << " Received: " << responses.size();
return ZX_ERR_INVALID_ARGS;
}
for (size_t i = 0; i < responses.size(); ++i) {
const auto& response = responses.at(i);
if (!return_tickets.at(i).expects_response) {
if (!response.IsEmpty()) {
FX_LOGS(ERROR) << "TouchSource: Expected empty response, receive non-empty response";
return ZX_ERR_INVALID_ARGS;
}
} else {
if (!response.has_response_type()) {
FX_LOGS(ERROR) << "TouchSource: Response was missing arguments.";
return ZX_ERR_INVALID_ARGS;
}
if (ConvertToGestureResponse(response.response_type()) == GestureResponse::kUndefined) {
FX_LOGS(ERROR) << "TouchSource: Response " << i << " had unknown response type.";
return ZX_ERR_INVALID_ARGS;
}
}
}
return ZX_OK;
}
void TouchSource::Watch(std::vector<fuchsia::ui::pointer::TouchResponse> responses,
WatchCallback callback) {
TRACE_DURATION("input", "TouchSource::Watch");
const zx_status_t error = ValidateResponses(
responses, return_tickets_, /*have_pending_callback*/ pending_callback_ != nullptr);
if (error != ZX_OK) {
CloseChannel(error);
return;
}
// De-interlace responses from different streams.
std::unordered_map<StreamId, std::vector<GestureResponse>> responses_per_stream;
size_t index = 0;
for (const auto& response : responses) {
if (response.has_trace_flow_id()) {
TRACE_FLOW_END("input", "received_response", response.trace_flow_id());
}
const auto [stream_id, expects_response] = return_tickets_.at(index++);
if (!expects_response) {
continue;
}
const GestureResponse gd_response = ConvertToGestureResponse(response.response_type());
responses_per_stream[stream_id].emplace_back(gd_response);
auto& stream = ongoing_streams_[stream_id];
stream.last_response = gd_response;
// TODO(fxbug.dev/53316): Remove when we no longer need to filter events.
// Duplicate the response for any subsequent filtered events.
++stream.num_responses;
while (!stream.filtered_events.empty() &&
stream.num_responses == stream.filtered_events.front() - 1) {
++stream.num_responses;
stream.filtered_events.pop();
responses_per_stream[stream_id].emplace_back(gd_response);
}
}
for (const auto& [stream_id, gd_responses] : responses_per_stream) {
respond_(stream_id, gd_responses);
}
pending_callback_ = std::move(callback);
return_tickets_.clear();
SendPendingIfWaiting();
}
zx_status_t TouchSource::ValidateUpdateResponse(
const fuchsia::ui::pointer::TouchInteractionId& stream_identifier,
const fuchsia::ui::pointer::TouchResponse& response,
const std::unordered_map<StreamId, StreamData>& ongoing_streams) {
const StreamId stream_id = stream_identifier.interaction_id;
if (ongoing_streams.count(stream_id) == 0) {
FX_LOGS(ERROR)
<< "TouchSource: Attempted to UpdateResponse for unkown stream. Received stream id: "
<< stream_id;
return ZX_ERR_BAD_STATE;
}
if (!response.has_response_type()) {
FX_LOGS(ERROR)
<< "TouchSource: Can only UpdateResponse() called without response_type argument.";
return ZX_ERR_INVALID_ARGS;
}
if (IsHold(response.response_type())) {
FX_LOGS(ERROR) << "TouchSource: Can only UpdateResponse() with non-HOLD response.";
return ZX_ERR_INVALID_ARGS;
}
const auto& stream = ongoing_streams.at(stream_id);
if (!IsHold(stream.last_response)) {
FX_LOGS(ERROR) << "TouchSource: Can only UpdateResponse() if previous response was HOLD.";
return ZX_ERR_BAD_STATE;
}
if (!stream.stream_has_ended) {
FX_LOGS(ERROR) << "TouchSource: Can only UpdateResponse() for ended streams.";
return ZX_ERR_BAD_STATE;
}
return ZX_OK;
}
void TouchSource::UpdateResponse(fuchsia::ui::pointer::TouchInteractionId stream_identifier,
fuchsia::ui::pointer::TouchResponse response,
UpdateResponseCallback callback) {
TRACE_DURATION("input", "TouchSource::UpdateResponse");
const zx_status_t error = ValidateUpdateResponse(stream_identifier, response, ongoing_streams_);
if (error != ZX_OK) {
CloseChannel(error);
return;
}
if (response.has_trace_flow_id()) {
TRACE_FLOW_END("input", "received_response", response.trace_flow_id());
}
const StreamId stream_id = stream_identifier.interaction_id;
const GestureResponse converted_response = ConvertToGestureResponse(response.response_type());
ongoing_streams_.at(stream_id).last_response = converted_response;
respond_(stream_id, {converted_response});
callback();
}
void TouchSource::SendPendingIfWaiting() {
if (!pending_callback_ || pending_events_.empty()) {
return;
}
FX_DCHECK(return_tickets_.empty());
std::vector<fuchsia::ui::pointer::TouchEvent> events;
for (size_t i = 0; !pending_events_.empty() && i < fuchsia::ui::pointer::TOUCH_MAX_EVENT; ++i) {
auto [stream_id, event] = std::move(pending_events_.front());
TRACE_FLOW_BEGIN("input", "dispatch_event_to_client", event.trace_flow_id());
pending_events_.pop();
return_tickets_.push_back(
{.stream_id = stream_id, .expects_response = event.has_pointer_sample()});
events.emplace_back(std::move(event));
}
FX_DCHECK(!events.empty());
FX_DCHECK(events.size() == return_tickets_.size());
pending_callback_(std::move(events));
pending_callback_ = nullptr;
}
void TouchSource::CloseChannel(zx_status_t epitaph) {
// NOTE: Triggers destruction of this object.
binding_.Close(epitaph);
}
} // namespace scenic_impl::input