blob: 79a462e5d140166d4333a55e0c40bc9d513bbd18 [file] [log] [blame]
// 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