| // Copyright 2016 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 "core/input/TouchEventManager.h" |
| |
| #include "core/dom/Document.h" |
| #include "core/events/TouchEvent.h" |
| #include "core/frame/Deprecation.h" |
| #include "core/frame/EventHandlerRegistry.h" |
| #include "core/frame/FrameHost.h" |
| #include "core/frame/FrameView.h" |
| #include "core/html/HTMLCanvasElement.h" |
| #include "core/input/EventHandler.h" |
| #include "core/input/TouchActionUtil.h" |
| #include "core/page/ChromeClient.h" |
| #include "core/page/Page.h" |
| #include "platform/Histogram.h" |
| #include "platform/PlatformTouchEvent.h" |
| #include "wtf/CurrentTime.h" |
| #include "wtf/PtrUtil.h" |
| #include <memory> |
| |
| |
| namespace blink { |
| |
| namespace { |
| |
| bool hasTouchHandlers(const EventHandlerRegistry& registry) |
| { |
| return registry.hasEventHandlers(EventHandlerRegistry::TouchStartOrMoveEventBlocking) |
| || registry.hasEventHandlers(EventHandlerRegistry::TouchStartOrMoveEventPassive) |
| || registry.hasEventHandlers(EventHandlerRegistry::TouchEndOrCancelEventBlocking) |
| || registry.hasEventHandlers(EventHandlerRegistry::TouchEndOrCancelEventPassive); |
| } |
| |
| const AtomicString& touchEventNameForTouchPointState(PlatformTouchPoint::TouchState state) |
| { |
| switch (state) { |
| case PlatformTouchPoint::TouchReleased: |
| return EventTypeNames::touchend; |
| case PlatformTouchPoint::TouchCancelled: |
| return EventTypeNames::touchcancel; |
| case PlatformTouchPoint::TouchPressed: |
| return EventTypeNames::touchstart; |
| case PlatformTouchPoint::TouchMoved: |
| return EventTypeNames::touchmove; |
| case PlatformTouchPoint::TouchStationary: |
| // Fall through to default |
| default: |
| ASSERT_NOT_REACHED(); |
| return emptyAtom; |
| } |
| } |
| |
| enum TouchEventDispatchResultType { |
| UnhandledTouches, // Unhandled touch events. |
| HandledTouches, // Handled touch events. |
| TouchEventDispatchResultTypeMax, |
| }; |
| |
| // Defining this class type local to dispatchTouchEvents() and annotating |
| // it with STACK_ALLOCATED(), runs into MSVC(VS 2013)'s C4822 warning |
| // that the local class doesn't provide a local definition for 'operator new'. |
| // Which it intentionally doesn't and shouldn't. |
| // |
| // Work around such toolchain bugginess by lifting out the type, thereby |
| // taking it out of C4822's reach. |
| class ChangedTouches final { |
| STACK_ALLOCATED(); |
| public: |
| // The touches corresponding to the particular change state this struct |
| // instance represents. |
| Member<TouchList> m_touches; |
| |
| using EventTargetSet = HeapHashSet<Member<EventTarget>>; |
| // Set of targets involved in m_touches. |
| EventTargetSet m_targets; |
| }; |
| |
| } // namespace |
| |
| TouchEventManager::TouchEventManager(LocalFrame* frame) |
| : m_frame(frame) |
| { |
| clear(); |
| } |
| |
| TouchEventManager::~TouchEventManager() |
| { |
| } |
| |
| WebInputEventResult TouchEventManager::dispatchTouchEvents( |
| const PlatformTouchEvent& event, |
| const HeapVector<TouchInfo>& touchInfos, |
| bool allTouchesReleased) |
| { |
| // Build up the lists to use for the |touches|, |targetTouches| and |
| // |changedTouches| attributes in the JS event. See |
| // http://www.w3.org/TR/touch-events/#touchevent-interface for how these |
| // lists fit together. |
| |
| // Holds the complete set of touches on the screen. |
| TouchList* touches = TouchList::create(); |
| |
| // A different view on the 'touches' list above, filtered and grouped by |
| // event target. Used for the |targetTouches| list in the JS event. |
| using TargetTouchesHeapMap = HeapHashMap<EventTarget*, Member<TouchList>>; |
| TargetTouchesHeapMap touchesByTarget; |
| |
| // Array of touches per state, used to assemble the |changedTouches| list. |
| ChangedTouches changedTouches[PlatformTouchPoint::TouchStateEnd]; |
| |
| for (unsigned i = 0; i < touchInfos.size(); ++i) { |
| const TouchInfo& touchInfo = touchInfos[i]; |
| const PlatformTouchPoint& point = touchInfo.point; |
| PlatformTouchPoint::TouchState pointState = point.state(); |
| |
| Touch* touch = Touch::create( |
| touchInfo.targetFrame.get(), |
| touchInfo.touchNode.get(), |
| point.id(), |
| point.screenPos(), |
| touchInfo.contentPoint, |
| touchInfo.adjustedRadius, |
| point.rotationAngle(), |
| point.force(), |
| touchInfo.region); |
| |
| // Ensure this target's touch list exists, even if it ends up empty, so |
| // it can always be passed to TouchEvent::Create below. |
| TargetTouchesHeapMap::iterator targetTouchesIterator = touchesByTarget.find(touchInfo.touchNode.get()); |
| if (targetTouchesIterator == touchesByTarget.end()) { |
| touchesByTarget.set(touchInfo.touchNode.get(), TouchList::create()); |
| targetTouchesIterator = touchesByTarget.find(touchInfo.touchNode.get()); |
| } |
| |
| // |touches| and |targetTouches| should only contain information about |
| // touches still on the screen, so if this point is released or |
| // cancelled it will only appear in the |changedTouches| list. |
| if (pointState != PlatformTouchPoint::TouchReleased && pointState != PlatformTouchPoint::TouchCancelled) { |
| touches->append(touch); |
| targetTouchesIterator->value->append(touch); |
| } |
| |
| // Now build up the correct list for |changedTouches|. |
| // Note that any touches that are in the TouchStationary state (e.g. if |
| // the user had several points touched but did not move them all) should |
| // never be in the |changedTouches| list so we do not handle them |
| // explicitly here. See https://bugs.webkit.org/show_bug.cgi?id=37609 |
| // for further discussion about the TouchStationary state. |
| if (pointState != PlatformTouchPoint::TouchStationary && touchInfo.knownTarget) { |
| ASSERT(pointState < PlatformTouchPoint::TouchStateEnd); |
| if (!changedTouches[pointState].m_touches) |
| changedTouches[pointState].m_touches = TouchList::create(); |
| changedTouches[pointState].m_touches->append(touch); |
| changedTouches[pointState].m_targets.add(touchInfo.touchNode); |
| } |
| } |
| |
| if (allTouchesReleased) { |
| m_touchSequenceDocument.clear(); |
| m_touchSequenceUserGestureToken.clear(); |
| } |
| |
| WebInputEventResult eventResult = WebInputEventResult::NotHandled; |
| |
| // Now iterate through the |changedTouches| list and |m_targets| within it, |
| // sending TouchEvents to the targets as required. |
| for (unsigned state = 0; state != PlatformTouchPoint::TouchStateEnd; ++state) { |
| if (!changedTouches[state].m_touches) |
| continue; |
| |
| const AtomicString& eventName(touchEventNameForTouchPointState(static_cast<PlatformTouchPoint::TouchState>(state))); |
| for (const auto& eventTarget : changedTouches[state].m_targets) { |
| EventTarget* touchEventTarget = eventTarget; |
| TouchEvent* touchEvent = TouchEvent::create( |
| touches, touchesByTarget.get(touchEventTarget), changedTouches[state].m_touches.get(), |
| eventName, touchEventTarget->toNode()->document().domWindow(), |
| event.getModifiers(), event.cancelable(), event.causesScrollingIfUncanceled(), event.touchStartOrFirstTouchMove(), event.timestamp()); |
| |
| DispatchEventResult domDispatchResult = touchEventTarget->dispatchEvent(touchEvent); |
| |
| // Only report for top level documents with a single touch on |
| // touch-start or the first touch-move. |
| if (event.touchStartOrFirstTouchMove() && touchInfos.size() == 1 && m_frame->isMainFrame()) { |
| |
| // Record the disposition and latency of touch starts and first touch moves before and after the page is fully loaded respectively. |
| int64_t latencyInMicros = static_cast<int64_t>((monotonicallyIncreasingTime() - event.timestamp()) * 1000000.0); |
| if (event.cancelable()) { |
| if (m_frame->document()->isLoadCompleted()) { |
| DEFINE_STATIC_LOCAL(EnumerationHistogram, touchDispositionsAfterPageLoadHistogram, ("Event.Touch.TouchDispositionsAfterPageLoad", TouchEventDispatchResultTypeMax)); |
| touchDispositionsAfterPageLoadHistogram.count((domDispatchResult != DispatchEventResult::NotCanceled) ? HandledTouches : UnhandledTouches); |
| |
| DEFINE_STATIC_LOCAL(CustomCountHistogram, eventLatencyAfterPageLoadHistogram, ("Event.Touch.TouchLatencyAfterPageLoad", 1, 100000000, 50)); |
| eventLatencyAfterPageLoadHistogram.count(latencyInMicros); |
| } else { |
| DEFINE_STATIC_LOCAL(EnumerationHistogram, touchDispositionsBeforePageLoadHistogram, ("Event.Touch.TouchDispositionsBeforePageLoad", TouchEventDispatchResultTypeMax)); |
| touchDispositionsBeforePageLoadHistogram.count((domDispatchResult != DispatchEventResult::NotCanceled) ? HandledTouches : UnhandledTouches); |
| |
| DEFINE_STATIC_LOCAL(CustomCountHistogram, eventLatencyBeforePageLoadHistogram, ("Event.Touch.TouchLatencyBeforePageLoad", 1, 100000000, 50)); |
| eventLatencyBeforePageLoadHistogram.count(latencyInMicros); |
| } |
| // Report the touch disposition there is no active fling animation. |
| DEFINE_STATIC_LOCAL(EnumerationHistogram, touchDispositionsOutsideFlingHistogram, ("Event.Touch.TouchDispositionsOutsideFling2", TouchEventDispatchResultTypeMax)); |
| touchDispositionsOutsideFlingHistogram.count((domDispatchResult != DispatchEventResult::NotCanceled) ? HandledTouches : UnhandledTouches); |
| } |
| |
| // Report the touch disposition when there is an active fling animation. |
| if (event.dispatchType() == PlatformEvent::ListenersForcedNonBlockingDueToFling) { |
| DEFINE_STATIC_LOCAL(EnumerationHistogram, touchDispositionsDuringFlingHistogram, ("Event.Touch.TouchDispositionsDuringFling2", TouchEventDispatchResultTypeMax)); |
| touchDispositionsDuringFlingHistogram.count(touchEvent->preventDefaultCalledOnUncancelableEvent() ? HandledTouches : UnhandledTouches); |
| } |
| } |
| eventResult = EventHandler::mergeEventResult(eventResult, |
| EventHandler::toWebInputEventResult(domDispatchResult)); |
| } |
| } |
| |
| if (allTouchesReleased) |
| m_touchScrollStarted = false; |
| |
| return eventResult; |
| } |
| |
| void TouchEventManager::updateTargetAndRegionMapsForTouchStarts( |
| HeapVector<TouchInfo>& touchInfos) |
| { |
| for (auto& touchInfo : touchInfos) { |
| // Touch events implicitly capture to the touched node, and don't change |
| // active/hover states themselves (Gesture events do). So we only need |
| // to hit-test on touchstart and when the target could be different than |
| // the corresponding pointer event target. |
| if (touchInfo.point.state() == PlatformTouchPoint::TouchPressed) { |
| HitTestRequest::HitTestRequestType hitType = HitTestRequest::TouchEvent | HitTestRequest::ReadOnly | HitTestRequest::Active; |
| HitTestResult result; |
| // For the touchPressed points hit-testing is done in |
| // PointerEventManager. If it was the second touch there is a |
| // capturing documents for the touch and |m_touchSequenceDocument| |
| // is not null. So if PointerEventManager should hit-test again |
| // against |m_touchSequenceDocument| if the target set by |
| // PointerEventManager was either null or not in |
| // |m_touchSequenceDocument|. |
| if (m_touchSequenceDocument && (!touchInfo.touchNode |
| || &touchInfo.touchNode->document() != m_touchSequenceDocument)) { |
| if (m_touchSequenceDocument->frame()) { |
| LayoutPoint framePoint = roundedLayoutPoint(m_touchSequenceDocument->frame()->view()->rootFrameToContents(touchInfo.point.pos())); |
| result = EventHandler::hitTestResultInFrame(m_touchSequenceDocument->frame(), framePoint, hitType); |
| Node* node = result.innerNode(); |
| if (!node) |
| continue; |
| if (isHTMLCanvasElement(node)) { |
| std::pair<Element*, String> regionInfo = toHTMLCanvasElement(node)->getControlAndIdIfHitRegionExists(result.pointInInnerNodeFrame()); |
| if (regionInfo.first) |
| node = regionInfo.first; |
| touchInfo.region = regionInfo.second; |
| } |
| // Touch events should not go to text nodes. |
| if (node->isTextNode()) |
| node = FlatTreeTraversal::parent(*node); |
| touchInfo.touchNode = node; |
| } else { |
| continue; |
| } |
| } |
| if (!touchInfo.touchNode) |
| continue; |
| if (!m_touchSequenceDocument) { |
| // Keep track of which document should receive all touch events |
| // in the active sequence. This must be a single document to |
| // ensure we don't leak Nodes between documents. |
| m_touchSequenceDocument = &(touchInfo.touchNode->document()); |
| ASSERT(m_touchSequenceDocument->frame()->view()); |
| } |
| |
| // Ideally we'd ASSERT(!m_targetForTouchID.contains(point.id()) |
| // since we shouldn't get a touchstart for a touch that's already |
| // down. However EventSender allows this to be violated and there's |
| // some tests that take advantage of it. There may also be edge |
| // cases in the browser where this happens. |
| // See http://crbug.com/345372. |
| m_targetForTouchID.set(touchInfo.point.id(), touchInfo.touchNode); |
| |
| m_regionForTouchID.set(touchInfo.point.id(), touchInfo.region); |
| |
| TouchAction effectiveTouchAction = |
| TouchActionUtil::computeEffectiveTouchAction( |
| *touchInfo.touchNode); |
| if (effectiveTouchAction != TouchActionAuto) |
| m_frame->page()->chromeClient().setTouchAction(effectiveTouchAction); |
| } |
| } |
| } |
| |
| void TouchEventManager::setAllPropertiesOfTouchInfos( |
| HeapVector<TouchInfo>& touchInfos) |
| { |
| for (auto& touchInfo : touchInfos) { |
| PlatformTouchPoint::TouchState pointState = touchInfo.point.state(); |
| Node* touchNode = nullptr; |
| String regionID; |
| |
| if (pointState == PlatformTouchPoint::TouchReleased |
| || pointState == PlatformTouchPoint::TouchCancelled) { |
| // The target should be the original target for this touch, so get |
| // it from the hashmap. As it's a release or cancel we also remove |
| // it from the map. |
| touchNode = m_targetForTouchID.take(touchInfo.point.id()); |
| regionID = m_regionForTouchID.take(touchInfo.point.id()); |
| } else { |
| // No hittest is performed on move or stationary, since the target |
| // is not allowed to change anyway. |
| touchNode = m_targetForTouchID.get(touchInfo.point.id()); |
| regionID = m_regionForTouchID.get(touchInfo.point.id()); |
| } |
| |
| LocalFrame* targetFrame = nullptr; |
| bool knownTarget = false; |
| if (touchNode) { |
| Document& doc = touchNode->document(); |
| // If the target node has moved to a new document while it was being touched, |
| // we can't send events to the new document because that could leak nodes |
| // from one document to another. See http://crbug.com/394339. |
| if (&doc == m_touchSequenceDocument.get()) { |
| targetFrame = doc.frame(); |
| knownTarget = true; |
| } |
| } |
| if (!knownTarget) { |
| // If we don't have a target registered for the point it means we've |
| // missed our opportunity to do a hit test for it (due to some |
| // optimization that prevented blink from ever seeing the |
| // touchstart), or that the touch started outside the active touch |
| // sequence document. We should still include the touch in the |
| // Touches list reported to the application (eg. so it can |
| // differentiate between a one and two finger gesture), but we won't |
| // actually dispatch any events for it. Set the target to the |
| // Document so that there's some valid node here. Perhaps this |
| // should really be LocalDOMWindow, but in all other cases the target of |
| // a Touch is a Node so using the window could be a breaking change. |
| // Since we know there was no handler invoked, the specific target |
| // should be completely irrelevant to the application. |
| touchNode = m_touchSequenceDocument; |
| targetFrame = m_touchSequenceDocument->frame(); |
| } |
| ASSERT(targetFrame); |
| |
| // pagePoint should always be in the target element's document coordinates. |
| FloatPoint pagePoint = targetFrame->view()->rootFrameToContents( |
| touchInfo.point.pos()); |
| float scaleFactor = 1.0f / targetFrame->pageZoomFactor(); |
| |
| touchInfo.touchNode = touchNode; |
| touchInfo.targetFrame = targetFrame; |
| touchInfo.contentPoint = pagePoint.scaledBy(scaleFactor); |
| touchInfo.adjustedRadius = touchInfo.point.radius().scaledBy(scaleFactor); |
| touchInfo.knownTarget = knownTarget; |
| touchInfo.region = regionID; |
| } |
| } |
| |
| bool TouchEventManager::reHitTestTouchPointsIfNeeded( |
| const PlatformTouchEvent& event, |
| HeapVector<TouchInfo>& touchInfos) |
| { |
| bool newTouchSequence = true; |
| bool allTouchesReleased = true; |
| |
| for (const auto& point : event.touchPoints()) { |
| if (point.state() != PlatformTouchPoint::TouchPressed) |
| newTouchSequence = false; |
| if (point.state() != PlatformTouchPoint::TouchReleased && point.state() != PlatformTouchPoint::TouchCancelled) |
| allTouchesReleased = false; |
| } |
| if (newTouchSequence) { |
| // Ideally we'd ASSERT(!m_touchSequenceDocument) here since we should |
| // have cleared the active document when we saw the last release. But we |
| // have some tests that violate this, ClusterFuzz could trigger it, and |
| // there may be cases where the browser doesn't reliably release all |
| // touches. http://crbug.com/345372 tracks this. |
| m_touchSequenceDocument.clear(); |
| m_touchSequenceUserGestureToken.clear(); |
| } |
| |
| ASSERT(m_frame->view()); |
| if (m_touchSequenceDocument && (!m_touchSequenceDocument->frame() || !m_touchSequenceDocument->frame()->view())) { |
| // If the active touch document has no frame or view, it's probably being destroyed |
| // so we can't dispatch events. |
| return false; |
| } |
| |
| updateTargetAndRegionMapsForTouchStarts(touchInfos); |
| |
| m_touchPressed = !allTouchesReleased; |
| |
| // If there's no document receiving touch events, or no handlers on the |
| // document set to receive the events, then we can skip all the rest of |
| // this work. |
| if (!m_touchSequenceDocument || !m_touchSequenceDocument->frameHost() || !hasTouchHandlers(m_touchSequenceDocument->frameHost()->eventHandlerRegistry()) || !m_touchSequenceDocument->frame()) { |
| if (allTouchesReleased) { |
| m_touchSequenceDocument.clear(); |
| m_touchSequenceUserGestureToken.clear(); |
| } |
| return false; |
| } |
| |
| setAllPropertiesOfTouchInfos(touchInfos); |
| |
| return true; |
| } |
| |
| // TODO(rbyers): Replace with AutoReset as base/WTF unification permits. |
| class CurrentEventHolder { |
| // Always stack allocated to ensure lifetime doesn't exceed that of target |
| DISALLOW_NEW(); |
| public: |
| CurrentEventHolder(PlatformEvent::EventType& target, PlatformEvent::EventType value) |
| : m_target(target) |
| { |
| m_target = value; |
| } |
| ~CurrentEventHolder() |
| { |
| m_target = PlatformEvent::NoType; |
| } |
| private: |
| PlatformEvent::EventType& m_target; |
| }; |
| |
| WebInputEventResult TouchEventManager::handleTouchEvent( |
| const PlatformTouchEvent& event, |
| HeapVector<TouchInfo>& touchInfos) |
| { |
| // Track the current event for the scope of this function. |
| CurrentEventHolder holder(m_currentEvent, event.type()); |
| |
| if (!reHitTestTouchPointsIfNeeded(event, touchInfos)) |
| return WebInputEventResult::NotHandled; |
| |
| bool allTouchesReleased = true; |
| for (const auto& point : event.touchPoints()) { |
| if (point.state() != PlatformTouchPoint::TouchReleased |
| && point.state() != PlatformTouchPoint::TouchCancelled) |
| allTouchesReleased = false; |
| } |
| |
| // Whether a touch should be considered a "user gesture" or not is a tricky question. |
| // https://docs.google.com/document/d/1oF1T3O7_E4t1PYHV6gyCwHxOi3ystm0eSL5xZu7nvOg/edit# |
| |
| // The touchend corresponding to a tap is always a user gesture. |
| bool isTap = event.touchPoints().size() == 1 |
| && event.touchPoints()[0].state() == PlatformTouchPoint::TouchReleased |
| && !event.causesScrollingIfUncanceled(); |
| |
| // For now, disallow dragging as a user gesture when the events are being sent to a |
| // cross-origin iframe (crbug.com/582140). |
| bool isSameOrigin = false; |
| if (m_touchSequenceDocument && m_touchSequenceDocument->frame()) { |
| SecurityOrigin* securityOrigin = m_touchSequenceDocument->frame()->securityContext()->getSecurityOrigin(); |
| Frame* top = m_frame->tree().top(); |
| if (top && securityOrigin->canAccess(top->securityContext()->getSecurityOrigin())) |
| isSameOrigin = true; |
| } |
| |
| std::unique_ptr<UserGestureIndicator> gestureIndicator; |
| if (isTap || isSameOrigin) { |
| UserGestureUtilizedCallback* callback = 0; |
| // These are cases we'd like to migrate to not hold a user gesture. |
| if (event.type() == PlatformEvent::TouchStart |
| || event.type() == PlatformEvent::TouchMove |
| || (event.type() == PlatformEvent::TouchEnd && m_touchScrollStarted)) { |
| // Collect metrics in userGestureUtilized(). |
| callback = this; |
| } |
| if (m_touchSequenceUserGestureToken) |
| gestureIndicator = wrapUnique(new UserGestureIndicator(m_touchSequenceUserGestureToken.release(), callback)); |
| else |
| gestureIndicator = wrapUnique(new UserGestureIndicator(DefinitelyProcessingUserGesture, callback)); |
| m_touchSequenceUserGestureToken = UserGestureIndicator::currentToken(); |
| } |
| |
| return dispatchTouchEvents(event, touchInfos, allTouchesReleased); |
| } |
| |
| void TouchEventManager::clear() |
| { |
| m_touchSequenceDocument.clear(); |
| m_touchSequenceUserGestureToken.clear(); |
| m_targetForTouchID.clear(); |
| m_regionForTouchID.clear(); |
| m_touchPressed = false; |
| m_touchScrollStarted = false; |
| m_currentEvent = PlatformEvent::NoType; |
| } |
| |
| bool TouchEventManager::isAnyTouchActive() const |
| { |
| return m_touchPressed; |
| } |
| |
| DEFINE_TRACE(TouchEventManager) |
| { |
| visitor->trace(m_frame); |
| visitor->trace(m_touchSequenceDocument); |
| visitor->trace(m_targetForTouchID); |
| } |
| |
| void TouchEventManager::userGestureUtilized() |
| { |
| // This is invoked for UserGestureIndicators created in TouchEventManger::handleTouchEvent which perhaps |
| // represent touch actions which shouldn't be considered a user-gesture. Trigger a UseCounter based |
| // on the touch event that's currently being dispatched. |
| UseCounter::Feature feature; |
| |
| switch (m_currentEvent) { |
| case PlatformEvent::TouchStart: |
| feature = UseCounter::TouchStartUserGestureUtilized; |
| break; |
| case PlatformEvent::TouchMove: |
| feature = UseCounter::TouchMoveUserGestureUtilized; |
| break; |
| case PlatformEvent::TouchEnd: |
| feature = UseCounter::TouchEndDuringScrollUserGestureUtilized; |
| break; |
| default: |
| NOTREACHED(); |
| return; |
| } |
| Deprecation::countDeprecation(m_frame, feature); |
| } |
| |
| } // namespace blink |