| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/input/passthrough_touch_event_queue.h" |
| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "base/auto_reset.h" |
| #include "base/feature_list.h" |
| #include "base/metrics/field_trial_params.h" |
| #include "base/trace_event/trace_event.h" |
| #include "base/trace_event/typed_macros.h" |
| #include "components/input/touch_timeout_handler.h" |
| #include "components/input/web_touch_event_traits.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "ui/events/base_event_utils.h" |
| #include "ui/gfx/geometry/point_f.h" |
| |
| using blink::WebInputEvent; |
| using blink::WebTouchEvent; |
| using blink::WebTouchPoint; |
| using ui::LatencyInfo; |
| |
| namespace input { |
| namespace { |
| |
| // Compare all properties of touch points to determine the state. |
| bool HasPointChanged(const WebTouchPoint& point_1, |
| const WebTouchPoint& point_2) { |
| DCHECK_EQ(point_1.id, point_2.id); |
| if (point_1.PositionInScreen() != point_2.PositionInScreen() || |
| point_1.PositionInWidget() != point_2.PositionInWidget() || |
| point_1.radius_x != point_2.radius_x || |
| point_1.radius_y != point_2.radius_y || |
| point_1.rotation_angle != point_2.rotation_angle || |
| point_1.force != point_2.force || point_1.tilt_x != point_2.tilt_x || |
| point_1.tilt_y != point_2.tilt_y) { |
| return true; |
| } |
| return false; |
| } |
| |
| } // namespace |
| |
| PassthroughTouchEventQueue::Config::Config() = default; |
| PassthroughTouchEventQueue::Config::~Config() = default; |
| PassthroughTouchEventQueue::Config::Config( |
| const PassthroughTouchEventQueue::Config& other) = default; |
| |
| // static |
| const base::FeatureParam<std::string> |
| PassthroughTouchEventQueue::kSkipTouchEventFilterType{ |
| &blink::features::kSkipTouchEventFilter, |
| blink::features::kSkipTouchEventFilterTypeParamName, |
| blink::features::kSkipTouchEventFilterTypeParamValueDiscrete}; |
| |
| PassthroughTouchEventQueue::TouchEventWithLatencyInfoAndAckState:: |
| TouchEventWithLatencyInfoAndAckState( |
| const TouchEventWithLatencyInfo& event) |
| : TouchEventWithLatencyInfo(event), |
| ack_state_(blink::mojom::InputEventResultState::kUnknown) {} |
| |
| PassthroughTouchEventQueue::PassthroughTouchEventQueue( |
| PassthroughTouchEventQueueClient* client, |
| const Config& config) |
| : client_(client), |
| has_handlers_(true), |
| maybe_has_handler_for_current_sequence_(false), |
| drop_remaining_touches_in_sequence_(false), |
| send_touch_events_async_(false), |
| processing_acks_(false), |
| skip_touch_filter_(config.skip_touch_filter), |
| events_to_always_forward_(config.events_to_always_forward) { |
| if (config.touch_ack_timeout_supported) { |
| timeout_handler_ = std::make_unique<TouchTimeoutHandler>( |
| this, config.desktop_touch_ack_timeout_delay, |
| config.mobile_touch_ack_timeout_delay, config.task_runner); |
| } |
| } |
| |
| PassthroughTouchEventQueue::~PassthroughTouchEventQueue() = default; |
| |
| void PassthroughTouchEventQueue::SendTouchCancelEventForTouchEvent( |
| const TouchEventWithLatencyInfo& event_to_cancel) { |
| TouchEventWithLatencyInfo event = event_to_cancel; |
| WebTouchEventTraits::ResetTypeAndTouchStates( |
| WebInputEvent::Type::kTouchCancel, |
| // TODO(rbyers): Shouldn't we use a fresh timestamp? |
| event.event.TimeStamp(), &event.event); |
| { |
| ScopedDispatchToRendererCallback dispatch_callback( |
| client_->GetDispatchToRendererCallback()); |
| SendTouchEventImmediately(&event, false, dispatch_callback.callback); |
| } |
| } |
| |
| void PassthroughTouchEventQueue::QueueEvent( |
| const TouchEventWithLatencyInfo& event, |
| DispatchToRendererCallback& dispatch_callback) { |
| TRACE_EVENT0("input", "PassthroughTouchEventQueue::QueueEvent"); |
| |
| if (FilterBeforeForwarding(event.event) != PreFilterResult::kUnfiltered) { |
| std::move(dispatch_callback) |
| .Run(event.event, DispatchToRendererResult::kNotDispatched); |
| client_->OnFilteringTouchEvent(event.event); |
| |
| TouchEventWithLatencyInfoAndAckState event_with_ack_state = event; |
| event_with_ack_state.set_ack_info( |
| blink::mojom::InputEventResultSource::kBrowser, |
| blink::mojom::InputEventResultState::kNoConsumerExists); |
| outstanding_touches_.insert(event_with_ack_state); |
| AckCompletedEvents(); |
| return; |
| } |
| TouchEventWithLatencyInfo cloned_event(event); |
| SendTouchEventImmediately(&cloned_event, true, dispatch_callback); |
| } |
| |
| void PassthroughTouchEventQueue::PrependTouchScrollNotification( |
| uint32_t primary_unique_touch_event_id) { |
| TRACE_EVENT0("input", |
| "PassthroughTouchEventQueue::PrependTouchScrollNotification"); |
| |
| if (base::FeatureList::IsEnabled( |
| blink::features::kAsyncTouchMovesImmediatelyAfterScroll)) { |
| send_touch_events_async_ = true; |
| SetAckStateForPendingTouchMovesFromSequence(primary_unique_touch_event_id); |
| } |
| |
| TouchEventWithLatencyInfo touch( |
| WebInputEvent::Type::kTouchScrollStarted, WebInputEvent::kNoModifiers, |
| ui::EventTimeForNow(), LatencyInfo()); |
| touch.event.dispatch_type = WebInputEvent::DispatchType::kEventNonBlocking; |
| touch.event.unique_touch_event_id = ui::GetNextTouchEventId(); |
| { |
| ScopedDispatchToRendererCallback dispatch_callback( |
| client_->GetDispatchToRendererCallback()); |
| SendTouchEventImmediately(&touch, true, dispatch_callback.callback); |
| } |
| } |
| |
| void PassthroughTouchEventQueue::SetAckStateForPendingTouchMovesFromSequence( |
| uint32_t primary_unique_touch_event_id) { |
| if (curr_sequence_down_event_id_ != primary_unique_touch_event_id) { |
| // The touch sequence that started scroll has ended. |
| return; |
| } |
| |
| // Ack all outstanding touches in the current sequence to unblock the |
| // browser. |
| for (auto& it : outstanding_touches_) { |
| if (it.event.GetType() != WebInputEvent::Type::kTouchMove) { |
| break; |
| } |
| |
| auto& outstanding_touch = |
| const_cast<TouchEventWithLatencyInfoAndAckState&>(it); |
| if (it.ack_state() == blink::mojom::InputEventResultState::kUnknown) { |
| // Based on the CHECK above we are already in middle of processing acks, |
| // we can just set them ignored here and the existing loop can process |
| // acks for these events as well. |
| outstanding_touch.set_ack_info( |
| blink::mojom::InputEventResultSource::kBrowser, |
| blink::mojom::InputEventResultState::kIgnored); |
| } |
| } |
| } |
| |
| void PassthroughTouchEventQueue::ProcessTouchAck( |
| blink::mojom::InputEventResultSource ack_source, |
| blink::mojom::InputEventResultState ack_result, |
| const LatencyInfo& latency_info, |
| const uint32_t unique_touch_event_id, |
| bool should_stop_timeout_monitor) { |
| TRACE_EVENT0("input", "PassthroughTouchEventQueue::ProcessTouchAck"); |
| if (timeout_handler_ && |
| timeout_handler_->ConfirmTouchEvent(unique_touch_event_id, ack_result, |
| should_stop_timeout_monitor)) |
| return; |
| |
| auto touch_event_iter = outstanding_touches_.find(unique_touch_event_id); |
| if (touch_event_iter == outstanding_touches_.end()) { |
| TRACE_EVENT_INSTANT("input", "unique_touch_event_id NotFound"); |
| return; |
| } |
| |
| TouchEventWithLatencyInfoAndAckState& event = |
| const_cast<TouchEventWithLatencyInfoAndAckState&>(*touch_event_iter); |
| event.latency.AddNewLatencyFrom(latency_info); |
| event.set_ack_info(ack_source, ack_result); |
| |
| AckCompletedEvents(); |
| } |
| |
| void PassthroughTouchEventQueue::OnGestureEventAck( |
| const GestureEventWithLatencyInfo& event, |
| blink::mojom::InputEventResultState ack_result) { |
| // When the scroll finishes allow TouchEvents to be blocking again. |
| if (event.event.GetType() == blink::WebInputEvent::Type::kGestureScrollEnd) { |
| send_touch_events_async_ = false; |
| } else if (event.event.GetType() == |
| blink::WebInputEvent::Type::kGestureScrollUpdate && |
| ack_result == blink::mojom::InputEventResultState::kConsumed) { |
| send_touch_events_async_ = true; |
| } |
| } |
| |
| void PassthroughTouchEventQueue::OnHasTouchEventHandlers(bool has_handlers) { |
| has_handlers_ = has_handlers; |
| } |
| |
| bool PassthroughTouchEventQueue::IsPendingAckTouchStart() const { |
| if (outstanding_touches_.empty()) |
| return false; |
| |
| for (auto& iter : outstanding_touches_) { |
| if (iter.event.GetType() == WebInputEvent::Type::kTouchStart) |
| return true; |
| } |
| return false; |
| } |
| |
| void PassthroughTouchEventQueue::SetAckTimeoutEnabled(bool enabled) { |
| if (timeout_handler_) |
| timeout_handler_->SetEnabled(enabled); |
| } |
| |
| void PassthroughTouchEventQueue::SetIsMobileOptimizedSite( |
| bool mobile_optimized_site) { |
| if (timeout_handler_) |
| timeout_handler_->SetUseMobileTimeout(mobile_optimized_site); |
| } |
| |
| bool PassthroughTouchEventQueue::IsAckTimeoutEnabled() const { |
| return timeout_handler_ && timeout_handler_->IsEnabled(); |
| } |
| |
| bool PassthroughTouchEventQueue::Empty() const { |
| return outstanding_touches_.empty(); |
| } |
| |
| void PassthroughTouchEventQueue::FlushQueue() { |
| // Don't allow acks to be processed in AckCompletedEvents as that can |
| // interfere with gesture event dispatch ordering. |
| base::AutoReset<bool> process_acks(&processing_acks_, true); |
| drop_remaining_touches_in_sequence_ = true; |
| client_->FlushDeferredGestureQueue(); |
| while (!outstanding_touches_.empty()) { |
| auto iter = outstanding_touches_.begin(); |
| TouchEventWithLatencyInfoAndAckState event = *iter; |
| outstanding_touches_.erase(iter); |
| if (event.ack_state() == blink::mojom::InputEventResultState::kUnknown) |
| event.set_ack_info( |
| blink::mojom::InputEventResultSource::kBrowser, |
| blink::mojom::InputEventResultState::kNoConsumerExists); |
| AckTouchEventToClient(event, event.ack_source(), event.ack_state()); |
| } |
| } |
| |
| void PassthroughTouchEventQueue::OnTouchActionFromMain() { |
| if (base::FeatureList::IsEnabled( |
| blink::features::kAsyncTouchMovesImmediatelyAfterScroll)) { |
| // It's possible a deferred scroll might have actually started upon |
| // receiving touch action from main. And as a result ack of some touch moves |
| // would have been set locally in Browser itself in |
| // `SetAckStateForPendingTouchMovesFromSequence`. |
| AckCompletedEvents(); |
| } |
| } |
| |
| void PassthroughTouchEventQueue::StopTimeoutMonitor() { |
| if (timeout_handler_) |
| timeout_handler_->StopTimeoutMonitor(); |
| } |
| |
| void PassthroughTouchEventQueue::AckCompletedEvents() { |
| // Don't allow re-entrancy into this method otherwise |
| // the ordering of acks won't be preserved. |
| if (processing_acks_) { |
| TRACE_EVENT_INSTANT("input", "ProcessingAcksAlready"); |
| return; |
| } |
| base::AutoReset<bool> process_acks(&processing_acks_, true); |
| while (!outstanding_touches_.empty()) { |
| auto iter = outstanding_touches_.begin(); |
| if (iter->ack_state() == blink::mojom::InputEventResultState::kUnknown) { |
| TRACE_EVENT_INSTANT("input", "Unknown InputEventResultState"); |
| break; |
| } |
| TouchEventWithLatencyInfoAndAckState event = *iter; |
| outstanding_touches_.erase(iter); |
| AckTouchEventToClient(event, event.ack_source(), event.ack_state()); |
| } |
| } |
| |
| void PassthroughTouchEventQueue::AckTouchEventToClient( |
| const TouchEventWithLatencyInfo& acked_event, |
| blink::mojom::InputEventResultSource ack_source, |
| blink::mojom::InputEventResultState ack_result) { |
| UpdateTouchConsumerStates(acked_event.event, ack_result); |
| |
| // Skip ack for TouchScrollStarted since it was synthesized within the queue. |
| if (acked_event.event.GetType() != WebInputEvent::Type::kTouchScrollStarted) { |
| client_->OnTouchEventAck(acked_event, ack_source, ack_result); |
| } |
| } |
| |
| void PassthroughTouchEventQueue::SendTouchEventImmediately( |
| TouchEventWithLatencyInfo* touch, |
| bool wait_for_ack, |
| DispatchToRendererCallback& dispatch_callback) { |
| // Note: Touchstart events are marked cancelable to allow transitions between |
| // platform scrolling and JS pinching. Touchend events, however, remain |
| // uncancelable, mitigating the risk of jank when transitioning to a fling. |
| if (send_touch_events_async_ && |
| touch->event.GetType() != WebInputEvent::Type::kTouchStart) |
| touch->event.dispatch_type = WebInputEvent::DispatchType::kEventNonBlocking; |
| |
| if (touch->event.GetType() == WebInputEvent::Type::kTouchStart) |
| touch->event.touch_start_or_first_touch_move = true; |
| |
| // For touchmove events, compare touch points position from current event |
| // to last sent event and update touch points state. |
| if (touch->event.GetType() == WebInputEvent::Type::kTouchMove) { |
| CHECK(last_sent_touchevent_); |
| if (last_sent_touchevent_->GetType() == WebInputEvent::Type::kTouchStart) |
| touch->event.touch_start_or_first_touch_move = true; |
| for (unsigned int i = 0; i < last_sent_touchevent_->touches_length; ++i) { |
| const WebTouchPoint& last_touch_point = last_sent_touchevent_->touches[i]; |
| // Touches with same id may not have same index in Touches array. |
| for (unsigned int j = 0; j < touch->event.touches_length; ++j) { |
| const WebTouchPoint& current_touchmove_point = touch->event.touches[j]; |
| if (current_touchmove_point.id != last_touch_point.id) |
| continue; |
| |
| if (!HasPointChanged(last_touch_point, current_touchmove_point)) |
| touch->event.touches[j].state = |
| WebTouchPoint::State::kStateStationary; |
| |
| break; |
| } |
| } |
| } |
| |
| if (touch->event.GetType() != WebInputEvent::Type::kTouchScrollStarted) { |
| if (last_sent_touchevent_) |
| *last_sent_touchevent_ = touch->event; |
| else |
| last_sent_touchevent_ = std::make_unique<WebTouchEvent>(touch->event); |
| } |
| |
| if (touch->event.IsTouchSequenceStart()) { |
| curr_sequence_down_event_id_ = touch->event.unique_touch_event_id; |
| } else if (touch->event.IsTouchSequenceEnd()) { |
| curr_sequence_down_event_id_.reset(); |
| } |
| |
| if (timeout_handler_) |
| timeout_handler_->StartIfNecessary(*touch); |
| touch->event.GetModifiableEventLatencyMetadata().dispatched_to_renderer = |
| base::TimeTicks::Now(); |
| if (wait_for_ack) |
| outstanding_touches_.insert(*touch); |
| client_->SendTouchEventImmediately(*touch, dispatch_callback); |
| } |
| |
| PassthroughTouchEventQueue::PreFilterResult |
| PassthroughTouchEventQueue::FilterBeforeForwarding(const WebTouchEvent& event) { |
| PreFilterResult result = FilterBeforeForwardingImpl(event); |
| if (result == PreFilterResult::kFilteredTimeout || |
| result == PreFilterResult::kFilteredNoNonstationaryPointers) |
| return result; |
| |
| // Override non-timeout filter results based on the Finch trial that bypasses |
| // the filter. We do this here so that the event still has the opportunity to |
| // update any internal state that's necessary to handle future events |
| // (i.e. future touch moves might be dropped, even if this touch start isn't |
| // due to a filter override). |
| if (!ShouldFilterForEvent(event)) |
| return PreFilterResult::kUnfiltered; |
| |
| return result; |
| } |
| |
| bool PassthroughTouchEventQueue::ShouldFilterForEvent( |
| const blink::WebTouchEvent& event) { |
| // Always run all filtering if the SkipTouchEventFilter is disabled. |
| if (!skip_touch_filter_) |
| return true; |
| // If the experiment is enabled and all events are forwarded, always skip |
| // filtering. |
| if (events_to_always_forward_ == |
| blink::features::kSkipTouchEventFilterTypeParamValueAll) |
| return false; |
| // If the experiment is enabled and only discrete events are forwarded, |
| // always run filtering for touchmove events only. |
| return event.GetType() == WebInputEvent::Type::kTouchMove; |
| } |
| |
| PassthroughTouchEventQueue::PreFilterResult |
| PassthroughTouchEventQueue::FilterBeforeForwardingImpl( |
| const WebTouchEvent& event) { |
| // Unconditionally apply the timeout filter to avoid exacerbating |
| // any responsiveness problems on the page. |
| if (timeout_handler_ && timeout_handler_->FilterEvent(event)) |
| return PreFilterResult::kFilteredTimeout; |
| |
| if (event.GetType() == WebInputEvent::Type::kTouchScrollStarted) |
| return PreFilterResult::kUnfiltered; |
| |
| if (event.IsTouchSequenceStart()) { |
| // We don't know if we have a handler until we get the ACK back so |
| // assume it is true. |
| maybe_has_handler_for_current_sequence_ = true; |
| send_touch_events_async_ = false; |
| last_sent_touchevent_.reset(); |
| |
| drop_remaining_touches_in_sequence_ = false; |
| if (!has_handlers_) { |
| drop_remaining_touches_in_sequence_ = true; |
| // If the SkipTouchEventFilter experiment is running, drop through to |
| // the loop that filters events with no nonstationary pointers below. |
| if (ShouldFilterForEvent(event)) |
| return PreFilterResult::kFilteredNoPageHandlers; |
| } |
| } |
| |
| if (drop_remaining_touches_in_sequence_ && |
| event.GetType() != WebInputEvent::Type::kTouchCancel && |
| // If the SkipTouchEventFilter experiment is running, drop through to |
| // the loop that filters events with no nonstationary pointers below. |
| ShouldFilterForEvent(event)) |
| return PreFilterResult::kFilteredNoPageHandlers; |
| |
| if (event.GetType() == WebInputEvent::Type::kTouchStart) { |
| if (has_handlers_ || maybe_has_handler_for_current_sequence_) |
| return PreFilterResult::kUnfiltered; |
| // If the SkipTouchEventFilter experiment is running, drop through to |
| // the loop that filters events with no nonstationary pointers below. |
| else if (ShouldFilterForEvent(event)) |
| return PreFilterResult::kFilteredNoPageHandlers; |
| } |
| |
| // If none of the touch points active in the current sequence have handlers, |
| // don't forward the touch event. |
| if (!maybe_has_handler_for_current_sequence_ && ShouldFilterForEvent(event)) |
| return PreFilterResult::kFilteredNoHandlerForSequence; |
| |
| // Only forward a touch if it has a non-stationary pointer that is active |
| // in the current touch sequence. |
| for (size_t i = 0; i < event.touches_length; ++i) { |
| const WebTouchPoint& point = event.touches[i]; |
| if (point.state == WebTouchPoint::State::kStateStationary) |
| continue; |
| |
| // |last_sent_touchevent_| will be non-null as long as there is an |
| // active touch sequence being forwarded to the renderer. |
| if (!last_sent_touchevent_) |
| continue; |
| |
| for (size_t j = 0; j < last_sent_touchevent_->touches_length; ++j) { |
| if (point.id != last_sent_touchevent_->touches[j].id) |
| continue; |
| |
| if (event.GetType() != WebInputEvent::Type::kTouchMove) |
| return PreFilterResult::kUnfiltered; |
| |
| // All pointers in TouchMove events may have state as StateMoved, |
| // even though none of the pointers have not changed in real. |
| // Forward these events when at least one pointer has changed. |
| if (HasPointChanged(last_sent_touchevent_->touches[j], point)) |
| return PreFilterResult::kUnfiltered; |
| |
| // This is a TouchMove event for which we have yet to find a |
| // non-stationary pointer. Continue checking the next pointers |
| // in the |event|. |
| break; |
| } |
| } |
| |
| return PreFilterResult::kFilteredNoNonstationaryPointers; |
| } |
| |
| void PassthroughTouchEventQueue::UpdateTouchConsumerStates( |
| const WebTouchEvent& event, |
| blink::mojom::InputEventResultState ack_result) { |
| if (event.GetType() == WebInputEvent::Type::kTouchStart) { |
| if (ack_result == blink::mojom::InputEventResultState::kConsumed) |
| send_touch_events_async_ = false; |
| |
| // Once we have the ack back for the sequence we know if there |
| // is a handler or not. Other touch-starts sent can upgrade |
| // whether we have a handler or not as well. |
| if (event.IsTouchSequenceStart()) { |
| maybe_has_handler_for_current_sequence_ = |
| ack_result != blink::mojom::InputEventResultState::kNoConsumerExists; |
| } else { |
| maybe_has_handler_for_current_sequence_ |= |
| ack_result != blink::mojom::InputEventResultState::kNoConsumerExists; |
| } |
| } else if (event.IsTouchSequenceEnd()) { |
| maybe_has_handler_for_current_sequence_ = false; |
| } |
| } |
| |
| size_t PassthroughTouchEventQueue::SizeForTesting() const { |
| return outstanding_touches_.size(); |
| } |
| |
| bool PassthroughTouchEventQueue::IsTimeoutRunningForTesting() const { |
| return timeout_handler_ && timeout_handler_->IsTimeoutTimerRunning(); |
| } |
| |
| } // namespace input |