| // Copyright 2015 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/site_engagement/content/site_engagement_helper.h" |
| |
| #include <utility> |
| #include <vector> |
| |
| #include "base/functional/bind.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/time/time.h" |
| #include "base/trace_event/trace_event.h" |
| #include "components/no_state_prefetch/browser/no_state_prefetch_contents.h" |
| #include "components/no_state_prefetch/browser/no_state_prefetch_manager.h" |
| #include "components/site_engagement/content/engagement_type.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/web_contents.h" |
| |
| namespace site_engagement { |
| |
| namespace { |
| |
| int g_seconds_to_pause_engagement_detection = 10; |
| int g_seconds_delay_after_navigation = 10; |
| int g_seconds_delay_after_media_starts = 10; |
| int g_seconds_delay_after_show = 5; |
| |
| } // anonymous namespace |
| |
| // static |
| void SiteEngagementService::Helper::SetSecondsBetweenUserInputCheck( |
| int seconds) { |
| g_seconds_to_pause_engagement_detection = seconds; |
| } |
| |
| // static |
| void SiteEngagementService::Helper::SetSecondsTrackingDelayAfterNavigation( |
| int seconds) { |
| g_seconds_delay_after_navigation = seconds; |
| } |
| |
| // static |
| void SiteEngagementService::Helper::SetSecondsTrackingDelayAfterShow( |
| int seconds) { |
| g_seconds_delay_after_show = seconds; |
| } |
| |
| SiteEngagementService::Helper::~Helper() { |
| if (web_contents()) { |
| input_tracker_.Stop(); |
| media_tracker_.Stop(); |
| } |
| } |
| |
| SiteEngagementService::Helper::PeriodicTracker::PeriodicTracker( |
| SiteEngagementService::Helper* helper) |
| : helper_(helper), pause_timer_(std::make_unique<base::OneShotTimer>()) {} |
| |
| SiteEngagementService::Helper::PeriodicTracker::~PeriodicTracker() = default; |
| |
| void SiteEngagementService::Helper::PeriodicTracker::Start( |
| base::TimeDelta initial_delay) { |
| StartTimer(initial_delay); |
| } |
| |
| void SiteEngagementService::Helper::PeriodicTracker::Pause() { |
| TrackingStopped(); |
| StartTimer(base::Seconds(g_seconds_to_pause_engagement_detection)); |
| } |
| |
| void SiteEngagementService::Helper::PeriodicTracker::Stop() { |
| TrackingStopped(); |
| pause_timer_->Stop(); |
| } |
| |
| bool SiteEngagementService::Helper::PeriodicTracker::IsTimerRunning() { |
| return pause_timer_->IsRunning(); |
| } |
| |
| void SiteEngagementService::Helper::PeriodicTracker::SetPauseTimerForTesting( |
| std::unique_ptr<base::OneShotTimer> timer) { |
| pause_timer_ = std::move(timer); |
| } |
| |
| void SiteEngagementService::Helper::PeriodicTracker::StartTimer( |
| base::TimeDelta delay) { |
| pause_timer_->Start( |
| FROM_HERE, delay, |
| base::BindOnce( |
| &SiteEngagementService::Helper::PeriodicTracker::TrackingStarted, |
| base::Unretained(this))); |
| } |
| |
| SiteEngagementService::Helper::InputTracker::InputTracker( |
| SiteEngagementService::Helper* helper, |
| content::WebContents* web_contents) |
| : PeriodicTracker(helper), |
| content::WebContentsObserver(web_contents), |
| is_tracking_(false) {} |
| |
| void SiteEngagementService::Helper::InputTracker::TrackingStarted() { |
| is_tracking_ = true; |
| } |
| |
| void SiteEngagementService::Helper::InputTracker::TrackingStopped() { |
| is_tracking_ = false; |
| } |
| |
| // Record that there was some user input, and defer handling of the input event. |
| // Once the timer finishes running, the callbacks detecting user input will be |
| // registered again. |
| void SiteEngagementService::Helper::InputTracker::DidGetUserInteraction( |
| const blink::WebInputEvent& event) { |
| if (!is_tracking_) |
| return; |
| |
| const blink::WebInputEvent::Type type = event.GetType(); |
| |
| // This switch has a default NOTREACHED case because it will not test all |
| // of the values of the WebInputEvent::Type enum (hence it won't require the |
| // compiler verifying that all cases are covered). |
| switch (type) { |
| // Only respond to key down events to avoid multiple triggering on a single |
| // input (e.g. keypress is a key down then key up). |
| case blink::WebInputEvent::Type::kRawKeyDown: |
| case blink::WebInputEvent::Type::kKeyDown: |
| helper()->RecordUserInput(EngagementType::kKeypress); |
| break; |
| case blink::WebInputEvent::Type::kMouseDown: |
| helper()->RecordUserInput(EngagementType::kMouse); |
| break; |
| case blink::WebInputEvent::Type::kTouchStart: |
| helper()->RecordUserInput(EngagementType::kTouchGesture); |
| break; |
| case blink::WebInputEvent::Type::kGestureScrollBegin: |
| helper()->RecordUserInput(EngagementType::kScroll); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| Pause(); |
| } |
| |
| SiteEngagementService::Helper::MediaTracker::MediaTracker( |
| SiteEngagementService::Helper* helper, |
| content::WebContents* web_contents) |
| : PeriodicTracker(helper), content::WebContentsObserver(web_contents) {} |
| |
| SiteEngagementService::Helper::MediaTracker::~MediaTracker() = default; |
| |
| void SiteEngagementService::Helper::MediaTracker::TrackingStarted() { |
| if (!active_media_players_.empty()) { |
| // TODO(dominickn): Consider treating OCCLUDED tabs like HIDDEN tabs when |
| // computing engagement score. They are currently treated as VISIBLE tabs to |
| // preserve old behavior. |
| helper()->RecordMediaPlaying(web_contents()->GetVisibility() == |
| content::Visibility::HIDDEN); |
| } |
| |
| Pause(); |
| } |
| |
| void SiteEngagementService::Helper::MediaTracker::PrimaryPageChanged( |
| content::Page& page) { |
| // Media stops playing on navigation, so clear our state. |
| active_media_players_.clear(); |
| } |
| |
| void SiteEngagementService::Helper::MediaTracker::MediaStartedPlaying( |
| const MediaPlayerInfo& media_info, |
| const content::MediaPlayerId& id) { |
| // Only begin engagement detection when media actually starts playing. |
| active_media_players_.push_back(id); |
| if (!IsTimerRunning()) |
| Start(base::Seconds(g_seconds_delay_after_media_starts)); |
| } |
| |
| void SiteEngagementService::Helper::MediaTracker::MediaStoppedPlaying( |
| const MediaPlayerInfo& media_info, |
| const content::MediaPlayerId& id, |
| WebContentsObserver::MediaStoppedReason reason) { |
| std::erase(active_media_players_, id); |
| } |
| |
| SiteEngagementService::Helper::Helper( |
| content::WebContents* web_contents, |
| prerender::NoStatePrefetchManager* prefetch_manager) |
| : content::WebContentsObserver(web_contents), |
| content::WebContentsUserData<SiteEngagementService::Helper>( |
| *web_contents), |
| input_tracker_(this, web_contents), |
| media_tracker_(this, web_contents), |
| service_(SiteEngagementService::Get(web_contents->GetBrowserContext())), |
| prefetch_manager_(prefetch_manager) {} |
| |
| void SiteEngagementService::Helper::RecordUserInput(EngagementType type) { |
| TRACE_EVENT0("SiteEngagement", "RecordUserInput"); |
| content::WebContents* contents = web_contents(); |
| if (contents) |
| service_->HandleUserInput(contents, type); |
| } |
| |
| void SiteEngagementService::Helper::RecordMediaPlaying(bool is_hidden) { |
| content::WebContents* contents = web_contents(); |
| if (contents) |
| service_->HandleMediaPlaying(contents, is_hidden); |
| } |
| |
| void SiteEngagementService::Helper::DidFinishNavigation( |
| content::NavigationHandle* handle) { |
| // Ignore uncommitted, non main-frame, same page, or error page navigations. |
| if (!handle->HasCommitted() || !handle->IsInPrimaryMainFrame() || |
| handle->IsSameDocument() || handle->IsErrorPage()) { |
| return; |
| } |
| |
| input_tracker_.Stop(); |
| media_tracker_.Stop(); |
| |
| // Ignore no-state prefetcher loads. This means that no-state prefetchers will |
| // not receive navigation engagement. The implications are as follows: |
| // |
| // - Instant search prefetchers from the omnibox trigger DidFinishNavigation |
| // twice: once for the prefetcher, and again when the page swaps in. The |
| // second trigger has transition GENERATED and receives navigation |
| // engagement. |
| // - Prefetchers initiated by <link rel="prerender"> (e.g. search results) are |
| // always assigned the LINK transition, which is ignored for navigation |
| // engagement. |
| // |
| // Prefetchers trigger WasShown() when they are swapped in, so input |
| // engagement will activate even if navigation engagement is not scored. |
| if (prefetch_manager_ && |
| prefetch_manager_->GetNoStatePrefetchContents(web_contents())) |
| return; |
| |
| service_->HandleNavigation(web_contents(), handle->GetPageTransition()); |
| |
| input_tracker_.Start(base::Seconds(g_seconds_delay_after_navigation)); |
| } |
| |
| void SiteEngagementService::Helper::OnVisibilityChanged( |
| content::Visibility visibility) { |
| // TODO(fdoray): Once the page visibility API [1] treats hidden and occluded |
| // documents the same way, consider stopping |input_tracker_| when |
| // |visibility| is OCCLUDED. https://crbug.com/668690 |
| // [1] https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API |
| if (visibility == content::Visibility::HIDDEN) { |
| input_tracker_.Stop(); |
| } else { |
| // Start a timer to track input if it isn't already running and input isn't |
| // already being tracked. |
| if (!input_tracker_.IsTimerRunning() && !input_tracker_.is_tracking()) { |
| input_tracker_.Start(base::Seconds(g_seconds_delay_after_show)); |
| } |
| } |
| } |
| |
| WEB_CONTENTS_USER_DATA_KEY_IMPL(SiteEngagementService::Helper); |
| |
| } // namespace site_engagement |