| // Copyright 2017 The Chromium 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 "content/browser/renderer_host/input/passthrough_touch_event_queue.h" |
| |
| #include <utility> |
| |
| #include "base/auto_reset.h" |
| #include "base/macros.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/trace_event/trace_event.h" |
| #include "content/browser/renderer_host/input/touch_timeout_handler.h" |
| #include "content/common/input/web_touch_event_traits.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 content { |
| 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::TouchEventWithLatencyInfoAndAckState:: |
| TouchEventWithLatencyInfoAndAckState(const TouchEventWithLatencyInfo& event) |
| : TouchEventWithLatencyInfo(event), |
| ack_state_(INPUT_EVENT_ACK_STATE_UNKNOWN) {} |
| |
| bool PassthroughTouchEventQueue::TouchEventWithLatencyInfoAndAckState:: |
| operator<(const TouchEventWithLatencyInfoAndAckState& other) const { |
| return event.unique_touch_event_id < other.event.unique_touch_event_id; |
| } |
| |
| PassthroughTouchEventQueue::PassthroughTouchEventQueue( |
| TouchEventQueueClient* 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) { |
| if (config.touch_ack_timeout_supported) { |
| timeout_handler_.reset( |
| new TouchTimeoutHandler(this, config.desktop_touch_ack_timeout_delay, |
| config.mobile_touch_ack_timeout_delay)); |
| } |
| } |
| |
| PassthroughTouchEventQueue::~PassthroughTouchEventQueue() {} |
| |
| void PassthroughTouchEventQueue::SendTouchCancelEventForTouchEvent( |
| const TouchEventWithLatencyInfo& event_to_cancel) { |
| TouchEventWithLatencyInfo event = event_to_cancel; |
| WebTouchEventTraits::ResetTypeAndTouchStates( |
| WebInputEvent::kTouchCancel, |
| // TODO(rbyers): Shouldn't we use a fresh timestamp? |
| event.event.TimeStampSeconds(), &event.event); |
| SendTouchEventImmediately(&event, false); |
| } |
| |
| void PassthroughTouchEventQueue::QueueEvent( |
| const TouchEventWithLatencyInfo& event) { |
| TRACE_EVENT0("input", "PassthroughTouchEventQueue::QueueEvent"); |
| PreFilterResult filter_result = FilterBeforeForwarding(event.event); |
| if (filter_result != FORWARD_TO_RENDERER) { |
| client_->OnFilteringTouchEvent(event.event); |
| |
| InputEventAckState ack_state = |
| filter_result == ACK_WITH_NO_CONSUMER_EXISTS |
| ? INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS |
| : INPUT_EVENT_ACK_STATE_NOT_CONSUMED; |
| TouchEventWithLatencyInfoAndAckState event_with_ack_state = event; |
| event_with_ack_state.set_ack_state(ack_state); |
| outstanding_touches_.insert(event_with_ack_state); |
| AckCompletedEvents(); |
| return; |
| } |
| TouchEventWithLatencyInfo cloned_event(event); |
| SendTouchEventImmediately(&cloned_event, true); |
| } |
| |
| void PassthroughTouchEventQueue::PrependTouchScrollNotification() { |
| TRACE_EVENT0("input", |
| "PassthroughTouchEventQueue::PrependTouchScrollNotification"); |
| |
| TouchEventWithLatencyInfo touch( |
| WebInputEvent::kTouchScrollStarted, WebInputEvent::kNoModifiers, |
| ui::EventTimeStampToSeconds(ui::EventTimeForNow()), LatencyInfo()); |
| touch.event.dispatch_type = WebInputEvent::kEventNonBlocking; |
| SendTouchEventImmediately(&touch, true); |
| } |
| |
| void PassthroughTouchEventQueue::ProcessTouchAck( |
| InputEventAckState ack_result, |
| const LatencyInfo& latency_info, |
| const uint32_t unique_touch_event_id) { |
| TRACE_EVENT0("input", "PassthroughTouchEventQueue::ProcessTouchAck"); |
| if (timeout_handler_ && |
| timeout_handler_->ConfirmTouchEvent(unique_touch_event_id, ack_result)) |
| return; |
| |
| auto touch_event_iter = outstanding_touches_.begin(); |
| while (touch_event_iter != outstanding_touches_.end()) { |
| if (unique_touch_event_id == touch_event_iter->event.unique_touch_event_id) |
| break; |
| ++touch_event_iter; |
| } |
| |
| if (touch_event_iter == outstanding_touches_.end()) |
| return; |
| |
| TouchEventWithLatencyInfoAndAckState event = *touch_event_iter; |
| touch_event_iter = outstanding_touches_.erase(touch_event_iter); |
| event.latency.AddNewLatencyFrom(latency_info); |
| event.set_ack_state(ack_result); |
| outstanding_touches_.insert(touch_event_iter, event); |
| |
| AckCompletedEvents(); |
| } |
| |
| void PassthroughTouchEventQueue::OnGestureScrollEvent( |
| const GestureEventWithLatencyInfo& gesture_event) { |
| if (gesture_event.event.GetType() == |
| blink::WebInputEvent::kGestureScrollUpdate && |
| gesture_event.event.resending_plugin_id == -1) { |
| send_touch_events_async_ = true; |
| } |
| } |
| |
| void PassthroughTouchEventQueue::OnGestureEventAck( |
| const GestureEventWithLatencyInfo& event, |
| InputEventAckState ack_result) { |
| // Turn events sent during gesture scrolls to be async. |
| if (event.event.GetType() == blink::WebInputEvent::kGestureScrollUpdate && |
| event.event.resending_plugin_id == -1) { |
| send_touch_events_async_ = (ack_result == INPUT_EVENT_ACK_STATE_CONSUMED); |
| } |
| } |
| |
| 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::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() { |
| drop_remaining_touches_in_sequence_ = true; |
| while (!outstanding_touches_.empty()) { |
| auto iter = outstanding_touches_.begin(); |
| TouchEventWithLatencyInfoAndAckState event = *iter; |
| outstanding_touches_.erase(iter); |
| if (event.ack_state() == INPUT_EVENT_ACK_STATE_UNKNOWN) |
| event.set_ack_state(INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS); |
| AckTouchEventToClient(event, event.ack_state()); |
| } |
| } |
| |
| void PassthroughTouchEventQueue::AckCompletedEvents() { |
| // Don't allow re-entrancy into this method otherwise |
| // the ordering of acks won't be preserved. |
| if (processing_acks_) |
| return; |
| base::AutoReset<bool> process_acks(&processing_acks_, true); |
| while (!outstanding_touches_.empty()) { |
| auto iter = outstanding_touches_.begin(); |
| if (iter->ack_state() == INPUT_EVENT_ACK_STATE_UNKNOWN) |
| break; |
| TouchEventWithLatencyInfoAndAckState event = *iter; |
| outstanding_touches_.erase(iter); |
| AckTouchEventToClient(event, event.ack_state()); |
| } |
| } |
| |
| void PassthroughTouchEventQueue::AckTouchEventToClient( |
| const TouchEventWithLatencyInfo& acked_event, |
| InputEventAckState 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::kTouchScrollStarted) { |
| client_->OnTouchEventAck(acked_event, ack_result); |
| } |
| } |
| |
| void PassthroughTouchEventQueue::SendTouchEventImmediately( |
| TouchEventWithLatencyInfo* touch, |
| bool wait_for_ack) { |
| // 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::kTouchStart) |
| touch->event.dispatch_type = WebInputEvent::kEventNonBlocking; |
| |
| if (touch->event.GetType() == WebInputEvent::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::kTouchMove) { |
| CHECK(last_sent_touchevent_); |
| if (last_sent_touchevent_->GetType() == WebInputEvent::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::kStateStationary; |
| |
| break; |
| } |
| } |
| } |
| |
| if (touch->event.GetType() != WebInputEvent::kTouchScrollStarted) { |
| if (last_sent_touchevent_) |
| *last_sent_touchevent_ = touch->event; |
| else |
| last_sent_touchevent_.reset(new WebTouchEvent(touch->event)); |
| } |
| |
| if (timeout_handler_) |
| timeout_handler_->StartIfNecessary(*touch); |
| if (wait_for_ack) |
| outstanding_touches_.insert(*touch); |
| client_->SendTouchEventImmediately(*touch); |
| } |
| |
| PassthroughTouchEventQueue::PreFilterResult |
| PassthroughTouchEventQueue::FilterBeforeForwarding(const WebTouchEvent& event) { |
| if (event.GetType() == WebInputEvent::kTouchScrollStarted) |
| return FORWARD_TO_RENDERER; |
| |
| if (WebTouchEventTraits::IsTouchSequenceStart(event)) { |
| // 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; |
| return ACK_WITH_NO_CONSUMER_EXISTS; |
| } |
| } |
| |
| if (timeout_handler_ && timeout_handler_->FilterEvent(event)) |
| return ACK_WITH_NO_CONSUMER_EXISTS; |
| |
| if (drop_remaining_touches_in_sequence_ && |
| event.GetType() != WebInputEvent::kTouchCancel) { |
| return ACK_WITH_NO_CONSUMER_EXISTS; |
| } |
| |
| if (event.GetType() == WebInputEvent::kTouchStart) { |
| return (has_handlers_ || maybe_has_handler_for_current_sequence_) |
| ? FORWARD_TO_RENDERER |
| : ACK_WITH_NO_CONSUMER_EXISTS; |
| } |
| |
| if (maybe_has_handler_for_current_sequence_) { |
| // 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::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::kTouchMove) |
| return FORWARD_TO_RENDERER; |
| |
| // 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 FORWARD_TO_RENDERER; |
| |
| // 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 ACK_WITH_NO_CONSUMER_EXISTS; |
| } |
| |
| void PassthroughTouchEventQueue::UpdateTouchConsumerStates( |
| const WebTouchEvent& event, |
| InputEventAckState ack_result) { |
| if (event.GetType() == WebInputEvent::kTouchStart) { |
| if (ack_result == INPUT_EVENT_ACK_STATE_CONSUMED) |
| 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 (WebTouchEventTraits::IsTouchSequenceStart(event)) { |
| maybe_has_handler_for_current_sequence_ = |
| ack_result != INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS; |
| } else { |
| maybe_has_handler_for_current_sequence_ |= |
| ack_result != INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS; |
| } |
| } else if (WebTouchEventTraits::IsTouchSequenceEnd(event)) { |
| 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(); |
| } |
| |
| const TouchEventWithLatencyInfo& |
| PassthroughTouchEventQueue::GetLatestEventForTesting() const { |
| return *outstanding_touches_.rbegin(); |
| } |
| |
| } // namespace content |