114cf2c1c656cdd95e9e70d6bb15a8a4dce228ca
[WebKit-https.git] / Source / WebCore / page / ios / ContentChangeObserver.cpp
1 /*
2  * Copyright (C) 2019 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1.  Redistributions of source code must retain the above copyright
8  *     notice, this list of conditions and the following disclaimer.
9  * 2.  Redistributions in binary form must reproduce the above copyright
10  *     notice, this list of conditions and the following disclaimer in the
11  *     documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY
14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16  * DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY
17  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
20  * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23  */
24
25 #include "config.h"
26 #include "ContentChangeObserver.h"
27
28 #if PLATFORM(IOS_FAMILY)
29 #include "Chrome.h"
30 #include "ChromeClient.h"
31 #include "DOMTimer.h"
32 #include "Document.h"
33 #include "HTMLImageElement.h"
34 #include "Logging.h"
35 #include "NodeRenderStyle.h"
36 #include "Page.h"
37 #include "RenderDescendantIterator.h"
38 #include "Settings.h"
39
40 namespace WebCore {
41
42 static const Seconds maximumDelayForTimers { 350_ms };
43 static const Seconds maximumDelayForTransitions { 300_ms };
44
45 static bool isConsideredHidden(const Element& element)
46 {
47     if (!element.renderStyle())
48         return true;
49
50     auto& style = *element.renderStyle();
51     if (style.display() == DisplayType::None)
52         return true;
53
54     if (style.visibility() == Visibility::Hidden)
55         return true;
56
57     if (!style.opacity())
58         return true;
59
60     auto width = style.logicalWidth();
61     auto height = style.logicalHeight();
62     if ((width.isFixed() && !width.value()) || (height.isFixed() && !height.value()))
63         return true;
64
65     auto top = style.logicalTop();
66     auto left = style.logicalLeft();
67     // FIXME: This is trying to check if the element is outside of the viewport. This is incorrect for many reasons.
68     if (left.isFixed() && width.isFixed() && -left.value() >= width.value())
69         return true;
70     if (top.isFixed() && height.isFixed() && -top.value() >= height.value())
71         return true;
72
73     // It's a common technique used to position content offscreen.
74     if (style.hasOutOfFlowPosition() && left.isFixed() && left.value() <= -999)
75         return true;
76
77     // FIXME: Check for other cases like zero height with overflow hidden.
78     auto maxHeight = style.maxHeight();
79     if (maxHeight.isFixed() && !maxHeight.value())
80         return true;
81
82     return false;
83 }
84
85 ContentChangeObserver::ContentChangeObserver(Document& document)
86     : m_document(document)
87     , m_contentObservationTimer([this] { completeDurationBasedContentObservation(); })
88 {
89 }
90
91 static void willNotProceedWithClick(Frame& mainFrame)
92 {
93     for (auto* frame = &mainFrame; frame; frame = frame->tree().traverseNext()) {
94         if (auto* document = frame->document())
95             document->contentChangeObserver().willNotProceedWithClick();
96     }
97 }
98
99 void ContentChangeObserver::didRecognizeLongPress(Frame& mainFrame)
100 {
101     LOG(ContentObservation, "didRecognizeLongPress: cancel ongoing content change observing.");
102     WebCore::willNotProceedWithClick(mainFrame);
103 }
104
105 void ContentChangeObserver::didPreventDefaultForEvent(Frame& mainFrame)
106 {
107     LOG(ContentObservation, "didPreventDefaultForEvent: cancel ongoing content change observing.");
108     WebCore::willNotProceedWithClick(mainFrame);
109 }
110
111 void ContentChangeObserver::startContentObservationForDuration(Seconds duration)
112 {
113     if (!m_document.settings().contentChangeObserverEnabled())
114         return;
115     ASSERT(!hasVisibleChangeState());
116     LOG_WITH_STREAM(ContentObservation, stream << "startContentObservationForDuration: start observing the content for " << duration.milliseconds() << "ms");
117     adjustObservedState(Event::StartedFixedObservationTimeWindow);
118     m_contentObservationTimer.startOneShot(duration);
119 }
120
121 void ContentChangeObserver::completeDurationBasedContentObservation()
122 {
123     LOG_WITH_STREAM(ContentObservation, stream << "completeDurationBasedContentObservation: complete duration based content observing ");
124     adjustObservedState(Event::EndedFixedObservationTimeWindow);
125 }
126
127 void ContentChangeObserver::didAddTransition(const Element& element, const Animation& transition)
128 {
129     if (!m_document.settings().contentChangeObserverEnabled())
130         return;
131     if (hasVisibleChangeState())
132         return;
133     if (!isObservingTransitions())
134         return;
135     if (!transition.isDurationSet() || !transition.isPropertySet())
136         return;
137     if (!isObservedPropertyForTransition(transition.property()))
138         return;
139     auto transitionEnd = Seconds { transition.duration() + std::max<double>(0, transition.isDelaySet() ? transition.delay() : 0) };
140     if (transitionEnd > maximumDelayForTransitions)
141         return;
142     if (!isConsideredHidden(element))
143         return;
144     // In case of multiple transitions, the first tranistion wins (and it has to produce a visible content change in order to show up as hover).
145     if (m_elementsWithTransition.contains(&element))
146         return;
147     LOG_WITH_STREAM(ContentObservation, stream << "didAddTransition: transition created on " << &element << " (" << transitionEnd.milliseconds() << "ms).");
148
149     m_elementsWithTransition.add(&element);
150     adjustObservedState(Event::AddedTransition);
151 }
152
153 void ContentChangeObserver::didFinishTransition(const Element& element, CSSPropertyID propertyID)
154 {
155     if (!isObservedPropertyForTransition(propertyID))
156         return;
157     if (!m_elementsWithTransition.take(&element))
158         return;
159     LOG_WITH_STREAM(ContentObservation, stream << "didFinishTransition: transition finished (" << &element << ").");
160
161     adjustObservedState(isConsideredHidden(element) ? Event::EndedTransition : Event::CompletedTransition);
162 }
163
164 void ContentChangeObserver::didRemoveTransition(const Element& element, CSSPropertyID propertyID)
165 {
166     if (!isObservedPropertyForTransition(propertyID))
167         return;
168     if (!m_elementsWithTransition.take(&element))
169         return;
170     LOG_WITH_STREAM(ContentObservation, stream << "didRemoveTransition: transition got interrupted (" << &element << ").");
171
172     adjustObservedState(Event::CanceledTransition);
173 }
174
175 void ContentChangeObserver::didInstallDOMTimer(const DOMTimer& timer, Seconds timeout, bool singleShot)
176 {
177     if (!m_document.settings().contentChangeObserverEnabled())
178         return;
179     if (m_document.activeDOMObjectsAreSuspended())
180         return;
181     if (timeout > maximumDelayForTimers || !singleShot)
182         return;
183     if (!isObservingDOMTimerScheduling())
184         return;
185     if (hasVisibleChangeState())
186         return;
187     LOG_WITH_STREAM(ContentObservation, stream << "didInstallDOMTimer: register this timer: (" << &timer << ") and observe when it fires.");
188
189     registerDOMTimer(timer);
190     adjustObservedState(Event::InstalledDOMTimer);
191 }
192
193 void ContentChangeObserver::didRemoveDOMTimer(const DOMTimer& timer)
194 {
195     if (!containsObservedDOMTimer(timer))
196         return;
197     LOG_WITH_STREAM(ContentObservation, stream << "removeDOMTimer: remove registered timer (" << &timer << ")");
198
199     unregisterDOMTimer(timer);
200     adjustObservedState(Event::RemovedDOMTimer);
201 }
202
203 void ContentChangeObserver::willNotProceedWithClick()
204 {
205     LOG(ContentObservation, "willNotProceedWithClick: click will not happen.");
206     adjustObservedState(Event::WillNotProceedWithClick);
207 }
208
209 void ContentChangeObserver::domTimerExecuteDidStart(const DOMTimer& timer)
210 {
211     if (!containsObservedDOMTimer(timer))
212         return;
213     LOG_WITH_STREAM(ContentObservation, stream << "startObservingDOMTimerExecute: start observing (" << &timer << ") timer callback.");
214
215     m_observedDomTimerIsBeingExecuted = true;
216     adjustObservedState(Event::StartedDOMTimerExecution);
217 }
218
219 void ContentChangeObserver::domTimerExecuteDidFinish(const DOMTimer& timer)
220 {
221     if (!m_observedDomTimerIsBeingExecuted)
222         return;
223     LOG_WITH_STREAM(ContentObservation, stream << "stopObservingDOMTimerExecute: stop observing (" << &timer << ") timer callback.");
224
225     m_observedDomTimerIsBeingExecuted = false;
226     unregisterDOMTimer(timer);
227     adjustObservedState(Event::EndedDOMTimerExecution);
228 }
229
230 void ContentChangeObserver::styleRecalcDidStart()
231 {
232     if (!isWaitingForStyleRecalc())
233         return;
234     LOG(ContentObservation, "startObservingStyleRecalc: start observing style recalc.");
235
236     m_isInObservedStyleRecalc = true;
237     adjustObservedState(Event::StartedStyleRecalc);
238 }
239
240 void ContentChangeObserver::styleRecalcDidFinish()
241 {
242     if (!m_isInObservedStyleRecalc)
243         return;
244     LOG(ContentObservation, "stopObservingStyleRecalc: stop observing style recalc");
245
246     m_isInObservedStyleRecalc = false;
247     adjustObservedState(Event::EndedStyleRecalc);
248 }
249
250 void ContentChangeObserver::stopObservingPendingActivities()
251 {
252     setShouldObserveNextStyleRecalc(false);
253     setShouldObserveDOMTimerScheduling(false);
254     setShouldObserveTransitions(false);
255     clearObservedDOMTimers();
256     clearObservedTransitions();
257 }
258
259 void ContentChangeObserver::reset()
260 {
261     stopObservingPendingActivities();
262     setHasNoChangeState();
263     setIsBetweenTouchEndAndMouseMoved(false);
264
265     m_touchEventIsBeingDispatched = false;
266     m_isInObservedStyleRecalc = false;
267     m_observedDomTimerIsBeingExecuted = false;
268     m_mouseMovedEventIsBeingDispatched = false;
269
270     m_contentObservationTimer.stop();
271 }
272
273 void ContentChangeObserver::didSuspendActiveDOMObjects()
274 {
275     LOG(ContentObservation, "didSuspendActiveDOMObjects");
276     reset();
277 }
278
279 void ContentChangeObserver::willDetachPage()
280 {
281     LOG(ContentObservation, "willDetachPage");
282     reset();
283 }
284
285 void ContentChangeObserver::contentVisibilityDidChange()
286 {
287     LOG(ContentObservation, "contentVisibilityDidChange: visible content change did happen.");
288     adjustObservedState(Event::ContentVisibilityChanged);
289 }
290
291 void ContentChangeObserver::touchEventDidStart(PlatformEvent::Type eventType)
292 {
293 #if ENABLE(TOUCH_EVENTS)
294     if (!m_document.settings().contentChangeObserverEnabled())
295         return;
296     if (eventType != PlatformEvent::Type::TouchStart)
297         return;
298     LOG(ContentObservation, "touchEventDidStart: touch start event started.");
299     m_touchEventIsBeingDispatched = true;
300     adjustObservedState(Event::StartedTouchStartEventDispatching);
301 #else
302     UNUSED_PARAM(eventType);
303 #endif
304 }
305
306 void ContentChangeObserver::touchEventDidFinish()
307 {
308 #if ENABLE(TOUCH_EVENTS)
309     if (!m_touchEventIsBeingDispatched)
310         return;
311     ASSERT(m_document.settings().contentChangeObserverEnabled());
312     LOG(ContentObservation, "touchEventDidFinish: touch start event finished.");
313     m_touchEventIsBeingDispatched = false;
314     adjustObservedState(Event::EndedTouchStartEventDispatching);
315 #endif
316 }
317
318 void ContentChangeObserver::mouseMovedDidStart()
319 {
320     if (!m_document.settings().contentChangeObserverEnabled())
321         return;
322     LOG(ContentObservation, "mouseMovedDidStart: mouseMoved started.");
323     m_mouseMovedEventIsBeingDispatched = true;
324     adjustObservedState(Event::StartedMouseMovedEventDispatching);
325 }
326
327 void ContentChangeObserver::mouseMovedDidFinish()
328 {
329     if (!m_mouseMovedEventIsBeingDispatched)
330         return;
331     ASSERT(m_document.settings().contentChangeObserverEnabled());
332     LOG(ContentObservation, "mouseMovedDidFinish: mouseMoved finished.");
333     adjustObservedState(Event::EndedMouseMovedEventDispatching);
334     m_mouseMovedEventIsBeingDispatched = false;
335 }
336
337 void ContentChangeObserver::setShouldObserveNextStyleRecalc(bool shouldObserve)
338 {
339     if (shouldObserve)
340         LOG(ContentObservation, "Wait until next style recalc fires.");
341     m_isWaitingForStyleRecalc = shouldObserve;
342 }
343
344 bool ContentChangeObserver::hasDeterminateState() const
345 {
346     if (hasVisibleChangeState())
347         return true;
348     return observedContentChange() == WKContentNoChange && !hasPendingActivity();
349 }
350
351 void ContentChangeObserver::adjustObservedState(Event event)
352 {
353     auto resetToStartObserving = [&] {
354         setHasNoChangeState();
355         clearObservedDOMTimers();
356         clearObservedTransitions();
357         setIsBetweenTouchEndAndMouseMoved(false);
358         setShouldObserveNextStyleRecalc(false);
359         setShouldObserveDOMTimerScheduling(false);
360         setShouldObserveTransitions(false);
361         ASSERT(!m_isInObservedStyleRecalc);
362         ASSERT(!m_observedDomTimerIsBeingExecuted);
363     };
364
365     auto adjustStateAndNotifyContentChangeIfNeeded = [&] {
366         // Demote to "no change" when there's no pending activity anymore.
367         if (observedContentChange() == WKContentIndeterminateChange && !hasPendingActivity())
368             setHasNoChangeState();
369
370         // Do not notify the client unless we couldn't make the decision synchronously.
371         if (m_mouseMovedEventIsBeingDispatched) {
372             LOG(ContentObservation, "adjustStateAndNotifyContentChangeIfNeeded: in mouseMoved call. No need to notify the client.");
373             return;
374         }
375         if (isBetweenTouchEndAndMouseMoved()) {
376             LOG(ContentObservation, "adjustStateAndNotifyContentChangeIfNeeded: Not reached mouseMoved yet. No need to notify the client.");
377             return;
378         }
379         if (!hasDeterminateState()) {
380             LOG(ContentObservation, "adjustStateAndNotifyContentChangeIfNeeded: not in a determined state yet.");
381             return;
382         }
383         LOG_WITH_STREAM(ContentObservation, stream << "adjustStateAndNotifyContentChangeIfNeeded: sending observedContentChange ->" << observedContentChange());
384         ASSERT(m_document.page());
385         ASSERT(m_document.frame());
386         m_document.page()->chrome().client().observedContentChange(*m_document.frame());
387     };
388
389     switch (event) {
390     case Event::StartedTouchStartEventDispatching:
391         resetToStartObserving();
392         setShouldObserveDOMTimerScheduling(true);
393         setShouldObserveTransitions(true);
394         break;
395     case Event::EndedTouchStartEventDispatching:
396         setShouldObserveDOMTimerScheduling(false);
397         setShouldObserveTransitions(false);
398         setIsBetweenTouchEndAndMouseMoved(true);
399         break;
400     case Event::WillNotProceedWithClick:
401         reset();
402         break;
403     case Event::StartedMouseMovedEventDispatching:
404         ASSERT(!m_document.hasPendingStyleRecalc());
405         if (!isBetweenTouchEndAndMouseMoved())
406             resetToStartObserving();
407         setIsBetweenTouchEndAndMouseMoved(false);
408         setShouldObserveDOMTimerScheduling(!hasVisibleChangeState());
409         setShouldObserveTransitions(!hasVisibleChangeState());
410         break;
411     case Event::EndedMouseMovedEventDispatching:
412         setShouldObserveDOMTimerScheduling(false);
413         setShouldObserveTransitions(false);
414         break;
415     case Event::StartedStyleRecalc:
416         setShouldObserveNextStyleRecalc(false);
417         FALLTHROUGH;
418     case Event::StartedDOMTimerExecution:
419         ASSERT(isObservationTimeWindowActive() || observedContentChange() == WKContentIndeterminateChange);
420         break;
421     case Event::InstalledDOMTimer:
422     case Event::StartedFixedObservationTimeWindow:
423     case Event::AddedTransition:
424         ASSERT(!hasVisibleChangeState());
425         setHasIndeterminateState();
426         break;
427     case Event::EndedDOMTimerExecution:
428         setShouldObserveNextStyleRecalc(m_document.hasPendingStyleRecalc());
429         FALLTHROUGH;
430     case Event::EndedStyleRecalc:
431     case Event::RemovedDOMTimer:
432     case Event::CanceledTransition:
433         if (!isObservationTimeWindowActive())
434             adjustStateAndNotifyContentChangeIfNeeded();
435         break;
436     case Event::EndedTransition:
437         // onAnimationEnd can be called while in the middle of resolving the document (synchronously) or
438         // asynchronously right before the style update is issued. It also means we don't know whether this animation ends up producing visible content yet. 
439         if (m_document.inStyleRecalc()) {
440             // We need to start observing this style change synchronously.
441             m_isInObservedStyleRecalc = true;
442         } else
443             setShouldObserveNextStyleRecalc(true);
444         break;
445     case Event::CompletedTransition:
446         // Set visibility flag on and report visible change synchronously or asynchronously depending whether we are in the middle of style recalc.
447         contentVisibilityDidChange();
448         if (m_document.inStyleRecalc())
449             m_isInObservedStyleRecalc = true;
450         else if (!isObservationTimeWindowActive())
451             adjustStateAndNotifyContentChangeIfNeeded();
452         break;
453     case Event::EndedFixedObservationTimeWindow:
454         adjustStateAndNotifyContentChangeIfNeeded();
455         break;
456     case Event::ContentVisibilityChanged:
457         setHasVisibleChangeState();
458         // Stop pending activities. We don't need to observe them anymore.
459         stopObservingPendingActivities();
460         break;
461     }
462 }
463
464 ContentChangeObserver::StyleChangeScope::StyleChangeScope(Document& document, const Element& element)
465     : m_contentChangeObserver(document.contentChangeObserver())
466     , m_element(element)
467     , m_hadRenderer(element.renderer())
468 {
469     if (m_contentChangeObserver.isObservingContentChanges() && !m_contentChangeObserver.hasVisibleChangeState())
470         m_wasHidden = isConsideredHidden(m_element);
471 }
472
473 ContentChangeObserver::StyleChangeScope::~StyleChangeScope()
474 {
475     auto changedFromHiddenToVisible = [&] {
476         return m_wasHidden && !isConsideredHidden(m_element);
477     };
478
479     if (changedFromHiddenToVisible() && isConsideredClickable())
480         m_contentChangeObserver.contentVisibilityDidChange();
481 }
482
483 bool ContentChangeObserver::StyleChangeScope::isConsideredClickable() const
484 {
485     if (m_element.isInUserAgentShadowTree())
486         return false;
487
488     auto& element = const_cast<Element&>(m_element);
489     if (is<HTMLImageElement>(element)) {
490         // This is required to avoid HTMLImageElement's touch callout override logic. See rdar://problem/48937767.
491         return element.Element::willRespondToMouseClickEvents();
492     }
493
494     auto willRespondToMouseClickEvents = element.willRespondToMouseClickEvents();
495     if (!m_hadRenderer || willRespondToMouseClickEvents)
496         return willRespondToMouseClickEvents;
497     // In case when the visible content already had renderers it's not sufficient to check the "newly visible" element only since it might just be the container for the clickable content.  
498     ASSERT(m_element.renderer());
499     for (auto& descendant : descendantsOfType<RenderElement>(*element.renderer())) {
500         if (!descendant.element())
501             continue;
502         if (descendant.element()->willRespondToMouseClickEvents())
503             return true;
504     }
505     return false;
506 }
507
508 #if ENABLE(TOUCH_EVENTS)
509 ContentChangeObserver::TouchEventScope::TouchEventScope(Document& document, PlatformEvent::Type eventType)
510     : m_contentChangeObserver(document.contentChangeObserver())
511 {
512     m_contentChangeObserver.touchEventDidStart(eventType);
513 }
514
515 ContentChangeObserver::TouchEventScope::~TouchEventScope()
516 {
517     m_contentChangeObserver.touchEventDidFinish();
518 }
519 #endif
520
521 ContentChangeObserver::MouseMovedScope::MouseMovedScope(Document& document)
522     : m_contentChangeObserver(document.contentChangeObserver())
523 {
524     m_contentChangeObserver.mouseMovedDidStart();
525 }
526
527 ContentChangeObserver::MouseMovedScope::~MouseMovedScope()
528 {
529     m_contentChangeObserver.mouseMovedDidFinish();
530 }
531
532 ContentChangeObserver::StyleRecalcScope::StyleRecalcScope(Document& document)
533     : m_contentChangeObserver(document.contentChangeObserver())
534 {
535     m_contentChangeObserver.styleRecalcDidStart();
536 }
537
538 ContentChangeObserver::StyleRecalcScope::~StyleRecalcScope()
539 {
540     m_contentChangeObserver.styleRecalcDidFinish();
541 }
542
543 ContentChangeObserver::DOMTimerScope::DOMTimerScope(Document* document, const DOMTimer& domTimer)
544     : m_contentChangeObserver(document ? &document->contentChangeObserver() : nullptr)
545     , m_domTimer(domTimer)
546 {
547     if (m_contentChangeObserver)
548         m_contentChangeObserver->domTimerExecuteDidStart(m_domTimer);
549 }
550
551 ContentChangeObserver::DOMTimerScope::~DOMTimerScope()
552 {
553     if (m_contentChangeObserver)
554         m_contentChangeObserver->domTimerExecuteDidFinish(m_domTimer);
555 }
556
557 }
558
559 #endif // PLATFORM(IOS_FAMILY)