| // 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_service.h" |
| |
| #include <stddef.h> |
| |
| #include <algorithm> |
| #include <utility> |
| |
| #include "base/functional/bind.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/metrics/field_trial.h" |
| #include "base/observer_list.h" |
| #include "base/strings/string_util.h" |
| #include "base/task/thread_pool.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "base/time/clock.h" |
| #include "base/time/default_clock.h" |
| #include "base/time/time.h" |
| #include "base/trace_event/trace_event.h" |
| #include "base/values.h" |
| #include "build/build_config.h" |
| #include "components/browsing_data/core/browsing_data_utils.h" |
| #include "components/content_settings/core/browser/host_content_settings_map.h" |
| #include "components/content_settings/core/common/content_settings_pattern.h" |
| #include "components/permissions/permissions_client.h" |
| #include "components/prefs/pref_registry_simple.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/security_interstitials/content/security_interstitial_tab_helper.h" |
| #include "components/site_engagement/content/engagement_type.h" |
| #include "components/site_engagement/content/site_engagement_metrics.h" |
| #include "components/site_engagement/content/site_engagement_observer.h" |
| #include "components/site_engagement/content/site_engagement_score.h" |
| #include "components/site_engagement/core/pref_names.h" |
| #include "components/user_prefs/user_prefs.h" |
| #include "components/webapps/browser/webapps_client.h" |
| #include "components/webapps/common/web_app_id.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/url_constants.h" |
| #include "url/gurl.h" |
| |
| #if BUILDFLAG(IS_ANDROID) |
| #include "components/site_engagement/content/android/site_engagement_service_android.h" |
| #endif |
| |
| namespace site_engagement { |
| |
| namespace { |
| |
| // Global bool to ensure we only update the parameters from variations once. |
| bool g_updated_from_variations = false; |
| |
| SiteEngagementService::ServiceProvider* g_service_provider = nullptr; |
| |
| // Length of time between metrics logging. |
| const int kMetricsIntervalInMinutes = 60; |
| |
| // A clock that keeps showing the time it was constructed with. |
| class StoppedClock : public base::Clock { |
| public: |
| explicit StoppedClock(base::Time time) : time_(time) {} |
| |
| StoppedClock(const StoppedClock&) = delete; |
| StoppedClock& operator=(const StoppedClock&) = delete; |
| |
| ~StoppedClock() override = default; |
| |
| protected: |
| // base::Clock: |
| base::Time Now() const override { return time_; } |
| |
| private: |
| const base::Time time_; |
| }; |
| |
| // Helper for fetching content settings for one type. |
| ContentSettingsForOneType GetContentSettingsFromBrowserContext( |
| content::BrowserContext* browser_context, |
| ContentSettingsType type) { |
| return permissions::PermissionsClient::Get() |
| ->GetSettingsMap(browser_context) |
| ->GetSettingsForOneType(type); |
| } |
| |
| // Returns the combined list of origins which either have site engagement |
| // data stored, or have other settings that would provide a score bonus. |
| std::set<GURL> GetEngagementOriginsFromContentSettings( |
| HostContentSettingsMap* map) { |
| std::set<GURL> urls; |
| |
| // Fetch URLs of sites with engagement details stored. |
| for (const auto& site : |
| map->GetSettingsForOneType(ContentSettingsType::SITE_ENGAGEMENT)) { |
| urls.insert(GURL(site.primary_pattern.ToString())); |
| } |
| |
| return urls; |
| } |
| |
| SiteEngagementScore CreateEngagementScoreImpl(base::Clock* clock, |
| const GURL& origin, |
| HostContentSettingsMap* map) { |
| return SiteEngagementScore(clock, origin, map); |
| } |
| |
| mojom::SiteEngagementDetails GetDetailsImpl(base::Clock* clock, |
| const GURL& origin, |
| HostContentSettingsMap* map) { |
| return CreateEngagementScoreImpl(clock, origin, map).GetDetails(); |
| } |
| |
| bool IsUrlInUrlSet(const GURL& url, |
| SiteEngagementService::URLSets::Type url_set) { |
| if ((url_set & SiteEngagementService::URLSets::HTTP) && |
| url.SchemeIsHTTPOrHTTPS()) { |
| return true; |
| } |
| |
| if ((url_set & SiteEngagementService::URLSets::WEB_UI) && |
| (url.SchemeIs(content::kChromeUIScheme) || |
| url.SchemeIs(content::kChromeUIUntrustedScheme))) { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| std::vector<mojom::SiteEngagementDetails> GetAllDetailsImpl( |
| base::Clock* clock, |
| HostContentSettingsMap* map, |
| SiteEngagementService::URLSets::Type url_set, |
| blink::mojom::EngagementLevel minimum_engagement_level) { |
| std::set<GURL> origins = GetEngagementOriginsFromContentSettings(map); |
| |
| std::vector<mojom::SiteEngagementDetails> details; |
| if (url_set & SiteEngagementService::URLSets::HTTP && |
| minimum_engagement_level == blink::mojom::EngagementLevel::NONE) { |
| details.reserve(origins.size()); |
| } |
| for (const GURL& origin : origins) { |
| if (!IsUrlInUrlSet(origin, url_set)) { |
| continue; |
| } |
| SiteEngagementScore score(CreateEngagementScoreImpl(clock, origin, map)); |
| if (SiteEngagementService::IsEngagementAtLeast(score.GetTotalScore(), |
| minimum_engagement_level)) { |
| details.push_back(score.GetDetails()); |
| } |
| } |
| return details; |
| } |
| |
| // Only accept a navigation event for engagement if it is one of: |
| // a. direct typed navigation |
| // b. clicking on an omnibox suggestion brought up by typing a keyword |
| // c. clicking on a bookmark or opening a bookmark app |
| // d. a custom search engine keyword search (e.g. Wikipedia search box added as |
| // search engine) |
| // e. an automatically generated top level navigation (e.g. command line |
| // navigation, in product help link). |
| bool IsEngagementNavigation(ui::PageTransition transition) { |
| return ui::PageTransitionCoreTypeIs(transition, ui::PAGE_TRANSITION_TYPED) || |
| ui::PageTransitionCoreTypeIs(transition, |
| ui::PAGE_TRANSITION_GENERATED) || |
| ui::PageTransitionCoreTypeIs(transition, |
| ui::PAGE_TRANSITION_AUTO_BOOKMARK) || |
| ui::PageTransitionCoreTypeIs(transition, |
| ui::PAGE_TRANSITION_KEYWORD_GENERATED) || |
| ui::PageTransitionCoreTypeIs(transition, |
| ui::PAGE_TRANSITION_AUTO_TOPLEVEL); |
| } |
| |
| } // namespace |
| |
| const char SiteEngagementService::kEngagementParams[] = "SiteEngagement"; |
| |
| // static |
| void SiteEngagementService::RegisterProfilePrefs(PrefRegistrySimple* registry) { |
| registry->RegisterInt64Pref(prefs::kSiteEngagementLastUpdateTime, 0, |
| PrefRegistry::LOSSY_PREF); |
| } |
| |
| // static |
| SiteEngagementService* SiteEngagementService::Get( |
| content::BrowserContext* context) { |
| DCHECK(g_service_provider); |
| return g_service_provider->GetSiteEngagementService(context); |
| } |
| |
| // static |
| void SiteEngagementService::SetServiceProvider(ServiceProvider* provider) { |
| DCHECK(provider); |
| DCHECK(!g_service_provider); |
| g_service_provider = provider; |
| } |
| |
| // static |
| void SiteEngagementService::ClearServiceProvider(ServiceProvider* provider) { |
| DCHECK(provider); |
| DCHECK_EQ(provider, g_service_provider); |
| g_service_provider = nullptr; |
| } |
| |
| // static |
| double SiteEngagementService::GetMaxPoints() { |
| return SiteEngagementScore::kMaxPoints; |
| } |
| |
| // static |
| bool SiteEngagementService::IsEnabled() { |
| const std::string group_name = |
| base::FieldTrialList::FindFullName(kEngagementParams); |
| return !base::StartsWith(group_name, "Disabled", |
| base::CompareCase::SENSITIVE); |
| } |
| |
| // static |
| double SiteEngagementService::GetScoreFromSettings( |
| HostContentSettingsMap* settings, |
| const GURL& origin) { |
| return SiteEngagementScore(base::DefaultClock::GetInstance(), origin, |
| settings) |
| .GetTotalScore(); |
| } |
| |
| // static |
| std::vector<mojom::SiteEngagementDetails> |
| SiteEngagementService::GetAllDetailsInBackground( |
| base::Time now, |
| scoped_refptr<HostContentSettingsMap> map, |
| URLSets::Type url_set, |
| blink::mojom::EngagementLevel minimum_engagement) { |
| StoppedClock clock(now); |
| base::AssertLongCPUWorkAllowed(); |
| return GetAllDetailsImpl(&clock, map.get(), url_set, minimum_engagement); |
| } |
| |
| // static |
| bool SiteEngagementService::IsEngagementAtLeast( |
| double score, |
| blink::mojom::EngagementLevel level) { |
| DCHECK_LT(SiteEngagementScore::GetMediumEngagementBoundary(), |
| SiteEngagementScore::GetHighEngagementBoundary()); |
| switch (level) { |
| case blink::mojom::EngagementLevel::NONE: |
| return true; |
| case blink::mojom::EngagementLevel::MINIMAL: |
| return score > 0; |
| case blink::mojom::EngagementLevel::LOW: |
| return score >= 1; |
| case blink::mojom::EngagementLevel::MEDIUM: |
| return score >= SiteEngagementScore::GetMediumEngagementBoundary(); |
| case blink::mojom::EngagementLevel::HIGH: |
| return score >= SiteEngagementScore::GetHighEngagementBoundary(); |
| case blink::mojom::EngagementLevel::MAX: |
| return score == SiteEngagementScore::kMaxPoints; |
| } |
| NOTREACHED(); |
| } |
| |
| SiteEngagementService::SiteEngagementService(content::BrowserContext* context) |
| : browser_context_(context), clock_(base::DefaultClock::GetInstance()) { |
| content::GetUIThreadTaskRunner({base::TaskPriority::BEST_EFFORT}) |
| ->PostTask(FROM_HERE, |
| base::BindOnce(&SiteEngagementService::AfterStartupTask, |
| weak_factory_.GetWeakPtr())); |
| |
| if (!g_updated_from_variations) { |
| SiteEngagementScore::UpdateFromVariations(kEngagementParams); |
| g_updated_from_variations = true; |
| } |
| } |
| |
| SiteEngagementService::~SiteEngagementService() { |
| // Clear any observers to avoid dangling pointers back to this object. |
| for (auto& observer : observer_list_) |
| observer.Observe(nullptr); |
| } |
| |
| blink::mojom::EngagementLevel SiteEngagementService::GetEngagementLevel( |
| const GURL& url) const { |
| if (IsLastEngagementStale()) |
| CleanupEngagementScores(true); |
| |
| return CreateEngagementScore(url).GetEngagementLevel(); |
| } |
| |
| std::vector<mojom::SiteEngagementDetails> SiteEngagementService::GetAllDetails( |
| URLSets::Type url_set) const { |
| if (IsLastEngagementStale()) |
| CleanupEngagementScores(true); |
| return GetAllDetailsImpl( |
| clock_, |
| permissions::PermissionsClient::Get()->GetSettingsMap(browser_context_), |
| url_set, blink::mojom::EngagementLevel::NONE); |
| } |
| |
| void SiteEngagementService::HandleNotificationInteraction(const GURL& url) { |
| if (!ShouldRecordEngagement(url)) |
| return; |
| |
| double old_score = GetScore(url); |
| AddPoints(url, SiteEngagementScore::GetNotificationInteractionPoints()); |
| |
| MaybeRecordMetrics(); |
| OnEngagementEvent(nullptr /* web_contents */, url, |
| EngagementType::kNotificationInteraction, old_score); |
| } |
| |
| bool SiteEngagementService::IsBootstrapped() const { |
| return GetTotalEngagementPoints() >= |
| SiteEngagementScore::GetBootstrapPoints(); |
| } |
| |
| void SiteEngagementService::AddObserver(SiteEngagementObserver* observer) { |
| observer_list_.AddObserver(observer); |
| } |
| |
| void SiteEngagementService::RemoveObserver(SiteEngagementObserver* observer) { |
| observer_list_.RemoveObserver(observer); |
| } |
| |
| void SiteEngagementService::ResetBaseScoreForURL(const GURL& url, |
| double score) { |
| SiteEngagementScore engagement_score = CreateEngagementScore(url); |
| engagement_score.Reset(score, clock_->Now()); |
| engagement_score.Commit(); |
| } |
| |
| void SiteEngagementService::SetLastShortcutLaunchTime( |
| content::WebContents* web_contents, |
| #if !BUILDFLAG(IS_ANDROID) |
| const webapps::AppId& app_id, |
| #endif |
| const GURL& url) { |
| double old_score = GetScore(url); |
| |
| SiteEngagementScore score = CreateEngagementScore(url); |
| |
| base::Time now = clock_->Now(); |
| |
| score.set_last_shortcut_launch_time(now); |
| score.Commit(); |
| |
| std::optional<webapps::AppId> web_app_id; |
| #if !BUILDFLAG(IS_ANDROID) |
| CHECK(!app_id.empty()); |
| web_app_id = app_id; |
| #endif |
| |
| OnEngagementEvent(web_contents, url, EngagementType::kWebappShortcutLaunch, |
| old_score, web_app_id); |
| } |
| |
| double SiteEngagementService::GetScore(const GURL& url) const { |
| return GetDetails(url).total_score; |
| } |
| |
| mojom::SiteEngagementDetails SiteEngagementService::GetDetails( |
| const GURL& url) const { |
| // Ensure that if engagement is stale, we clean things up before fetching the |
| // score. |
| if (IsLastEngagementStale()) |
| CleanupEngagementScores(true); |
| |
| return GetDetailsImpl( |
| clock_, url, |
| permissions::PermissionsClient::Get()->GetSettingsMap(browser_context_)); |
| } |
| |
| double SiteEngagementService::GetTotalEngagementPoints() const { |
| std::vector<mojom::SiteEngagementDetails> details = GetAllDetails(); |
| |
| double total_score = 0; |
| for (const auto& detail : details) |
| total_score += detail.total_score; |
| |
| return total_score; |
| } |
| |
| void SiteEngagementService::AddPointsForTesting(const GURL& url, |
| double points) { |
| AddPoints(url, points); |
| } |
| |
| #if BUILDFLAG(IS_ANDROID) |
| SiteEngagementServiceAndroid* SiteEngagementService::GetAndroidService() const { |
| return android_service_.get(); |
| } |
| |
| void SiteEngagementService::SetAndroidService( |
| std::unique_ptr<SiteEngagementServiceAndroid> android_service) { |
| android_service_ = std::move(android_service); |
| } |
| #endif |
| |
| void SiteEngagementService::AddPoints(const GURL& url, double points) { |
| if (points == 0) |
| return; |
| |
| // Trigger a cleanup and date adjustment if it has been a substantial length |
| // of time since *any* engagement was recorded by the service. This will |
| // ensure that we do not decay scores when the user did not use the browser. |
| if (IsLastEngagementStale()) |
| CleanupEngagementScores(true); |
| |
| SiteEngagementScore score = CreateEngagementScore(url); |
| score.AddPoints(points); |
| score.Commit(); |
| |
| SetLastEngagementTime(score.last_engagement_time()); |
| } |
| |
| void SiteEngagementService::AfterStartupTask() { |
| // Check if we need to reset last engagement times on startup - we want to |
| // avoid doing this in AddPoints() if possible. It is still necessary to check |
| // in AddPoints for people who never restart Chrome, but leave it open and |
| // their computer on standby. |
| CleanupEngagementScores(IsLastEngagementStale()); |
| } |
| |
| void SiteEngagementService::CleanupEngagementScores( |
| bool update_last_engagement_time) const { |
| TRACE_EVENT0("navigation", "SiteEngagementService::CleanupEngagementScores"); |
| |
| // We want to rebase last engagement times relative to MaxDecaysPerScore |
| // periods of decay in the past. |
| base::Time now = clock_->Now(); |
| base::Time last_engagement_time = GetLastEngagementTime(); |
| base::Time rebase_time = now - GetMaxDecayPeriod(); |
| base::Time new_last_engagement_time; |
| |
| // If |update_last_engagement_time| is true, we must have either: |
| // a) last_engagement_time is in the future; OR |
| // b) last_engagement_time < rebase_time < now |
| DCHECK(!update_last_engagement_time || last_engagement_time >= now || |
| (last_engagement_time < rebase_time && rebase_time < now)); |
| |
| // Cap |last_engagement_time| at |now| if it is in the future. This ensures |
| // that we use sane offsets when a user has adjusted their clock backwards and |
| // have a mix of scores prior to and after |now|. |
| if (last_engagement_time > now) |
| last_engagement_time = now; |
| |
| HostContentSettingsMap* settings_map = |
| permissions::PermissionsClient::Get()->GetSettingsMap(browser_context_); |
| for (const auto& site : GetContentSettingsFromBrowserContext( |
| browser_context_, ContentSettingsType::SITE_ENGAGEMENT)) { |
| GURL origin(site.primary_pattern.ToString()); |
| |
| if (origin.is_valid()) { |
| SiteEngagementScore score = CreateEngagementScore(origin); |
| if (update_last_engagement_time) { |
| // Catch cases of users moving their clocks, or a potential race where |
| // a score content setting is written out to prefs, but the updated |
| // |last_engagement_time| was not written, as both are lossy |
| // preferences. |rebase_time| is strictly in the past, so any score with |
| // a last updated time in the future is caught by this branch. |
| if (score.last_engagement_time() > rebase_time) { |
| score.SetLastEngagementTime(now); |
| } else if (score.last_engagement_time() > last_engagement_time) { |
| // This score is newer than |last_engagement_time|, but older than |
| // |rebase_time|. It should still be rebased with no offset as we |
| // don't accurately know what the offset should be. |
| score.SetLastEngagementTime(rebase_time); |
| } else { |
| // Work out the offset between this score's last engagement time and |
| // the last time the service recorded any engagement. Set the score's |
| // last engagement time to rebase_time - offset to preserve its state, |
| // relative to the rebase date. This ensures that the score will decay |
| // the next time it is used, but will not decay too much. |
| base::TimeDelta offset = |
| last_engagement_time - score.last_engagement_time(); |
| base::Time rebase_score_time = rebase_time - offset; |
| score.SetLastEngagementTime(rebase_score_time); |
| } |
| |
| if (score.last_engagement_time() > new_last_engagement_time) |
| new_last_engagement_time = score.last_engagement_time(); |
| score.Commit(); |
| } |
| |
| if (score.GetTotalScore() > |
| SiteEngagementScore::GetScoreCleanupThreshold()) |
| continue; |
| } |
| |
| // This origin has a score of 0. Wipe it from content settings. |
| settings_map->SetWebsiteSettingDefaultScope( |
| origin, GURL(), ContentSettingsType::SITE_ENGAGEMENT, base::Value()); |
| } |
| |
| // Set the last engagement time to be consistent with the scores. This will |
| // only occur if |update_last_engagement_time| is true. |
| if (!new_last_engagement_time.is_null()) |
| SetLastEngagementTime(new_last_engagement_time); |
| } |
| |
| void SiteEngagementService::MaybeRecordMetrics() { |
| base::Time now = clock_->Now(); |
| if (browser_context_->IsOffTheRecord() || |
| (!last_metrics_time_.is_null() && |
| (now - last_metrics_time_).InMinutes() < kMetricsIntervalInMinutes)) { |
| return; |
| } |
| |
| // Clean up engagement first before retrieving scores. |
| if (IsLastEngagementStale()) |
| CleanupEngagementScores(true); |
| |
| last_metrics_time_ = now; |
| |
| // Retrieve details on a background thread as this is expensive. We may end up |
| // with minor data inconsistency but this doesn't really matter for metrics |
| // purposes. |
| // |
| // The BrowserContext and its KeyedServices are normally destroyed before the |
| // ThreadPool shuts down background threads, so the task needs to hold a |
| // strong reference to HostContentSettingsMap (which supports outliving the |
| // browser context), and needs to avoid using any members of |
| // SiteEngagementService (which does not). See https://crbug.com/900022. |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, |
| {base::TaskPriority::BEST_EFFORT, |
| base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}, |
| base::BindOnce(&GetAllDetailsInBackground, now, |
| base::WrapRefCounted( |
| permissions::PermissionsClient::Get()->GetSettingsMap( |
| browser_context_)), |
| URLSets::HTTP, blink::mojom::EngagementLevel::NONE), |
| base::BindOnce(&SiteEngagementService::RecordMetrics, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void SiteEngagementService::RecordMetrics( |
| std::vector<mojom::SiteEngagementDetails> details) { |
| TRACE_EVENT0("navigation", "SiteEngagementService::RecordMetrics"); |
| std::sort(details.begin(), details.end(), |
| [](const mojom::SiteEngagementDetails& lhs, |
| const mojom::SiteEngagementDetails& rhs) { |
| return lhs.total_score < rhs.total_score; |
| }); |
| |
| int total_origins = details.size(); |
| |
| double total_engagement = 0; |
| for (const auto& detail : details) { |
| total_engagement += detail.total_score; |
| } |
| |
| double mean_engagement = |
| (total_origins == 0 ? 0 : total_engagement / total_origins); |
| |
| SiteEngagementMetrics::RecordTotalOriginsEngaged(total_origins); |
| SiteEngagementMetrics::RecordMeanEngagement(mean_engagement); |
| SiteEngagementMetrics::RecordMedianEngagement( |
| GetMedianEngagementFromSortedDetails(details)); |
| SiteEngagementMetrics::RecordEngagementScores(details); |
| } |
| |
| bool SiteEngagementService::ShouldRecordEngagement(const GURL& url) const { |
| return url.SchemeIsHTTPOrHTTPS() || url.SchemeIs(content::kChromeUIScheme) || |
| url.SchemeIs(content::kChromeUIUntrustedScheme); |
| } |
| |
| base::Time SiteEngagementService::GetLastEngagementTime() const { |
| if (browser_context_->IsOffTheRecord()) |
| return base::Time(); |
| |
| return base::Time::FromInternalValue( |
| user_prefs::UserPrefs::Get(browser_context_) |
| ->GetInt64(prefs::kSiteEngagementLastUpdateTime)); |
| } |
| |
| void SiteEngagementService::SetLastEngagementTime( |
| base::Time last_engagement_time) const { |
| if (browser_context_->IsOffTheRecord()) |
| return; |
| user_prefs::UserPrefs::Get(browser_context_) |
| ->SetInt64(prefs::kSiteEngagementLastUpdateTime, |
| last_engagement_time.ToInternalValue()); |
| } |
| |
| base::TimeDelta SiteEngagementService::GetMaxDecayPeriod() const { |
| return base::Hours(SiteEngagementScore::GetDecayPeriodInHours()) * |
| SiteEngagementScore::GetMaxDecaysPerScore(); |
| } |
| |
| base::TimeDelta SiteEngagementService::GetStalePeriod() const { |
| return GetMaxDecayPeriod() + |
| base::Hours( |
| SiteEngagementScore::GetLastEngagementGracePeriodInHours()); |
| } |
| |
| double SiteEngagementService::GetMedianEngagementFromSortedDetails( |
| const std::vector<mojom::SiteEngagementDetails>& details) const { |
| if (details.empty()) |
| return 0; |
| |
| // Calculate the median as the middle value of the sorted engagement scores |
| // if there are an odd number of scores, or the average of the two middle |
| // scores otherwise. |
| size_t mid = details.size() / 2; |
| if (details.size() % 2 == 1) |
| return details[mid].total_score; |
| else |
| return (details[mid - 1].total_score + details[mid].total_score) / 2; |
| } |
| |
| void SiteEngagementService::HandleMediaPlaying( |
| content::WebContents* web_contents, |
| bool is_hidden) { |
| const GURL& url = web_contents->GetLastCommittedURL(); |
| if (!ShouldRecordEngagement(url)) |
| return; |
| |
| double old_score = GetScore(url); |
| AddPoints(url, is_hidden ? SiteEngagementScore::GetHiddenMediaPoints() |
| : SiteEngagementScore::GetVisibleMediaPoints()); |
| |
| MaybeRecordMetrics(); |
| OnEngagementEvent( |
| web_contents, url, |
| is_hidden ? EngagementType::kMediaHidden : EngagementType::kMediaVisible, |
| old_score); |
| } |
| |
| void SiteEngagementService::HandleNavigation(content::WebContents* web_contents, |
| ui::PageTransition transition) { |
| const GURL& url = web_contents->GetLastCommittedURL(); |
| if (!IsEngagementNavigation(transition) || !ShouldRecordEngagement(url)) |
| return; |
| |
| double old_score = GetScore(url); |
| AddPoints(url, SiteEngagementScore::GetNavigationPoints()); |
| |
| MaybeRecordMetrics(); |
| OnEngagementEvent(web_contents, url, EngagementType::kNavigation, old_score); |
| } |
| |
| void SiteEngagementService::HandleUserInput(content::WebContents* web_contents, |
| EngagementType type) { |
| const GURL& url = web_contents->GetLastCommittedURL(); |
| if (!ShouldRecordEngagement(url)) |
| return; |
| |
| security_interstitials::SecurityInterstitialTabHelper* helper = |
| security_interstitials::SecurityInterstitialTabHelper::FromWebContents( |
| web_contents); |
| if (helper && helper->IsDisplayingInterstitial()) { |
| return; |
| } |
| |
| double old_score = GetScore(url); |
| AddPoints(url, SiteEngagementScore::GetUserInputPoints()); |
| |
| MaybeRecordMetrics(); |
| OnEngagementEvent(web_contents, url, type, old_score); |
| } |
| |
| void SiteEngagementService::OnEngagementEvent( |
| content::WebContents* web_contents, |
| const GURL& url, |
| EngagementType type, |
| double old_score, |
| const std::optional<webapps::AppId>& app_id_override) { |
| SiteEngagementMetrics::RecordEngagement(type); |
| |
| std::optional<webapps::AppId> app_id = app_id_override; |
| if (!app_id && web_contents && webapps::WebappsClient::Get()) { |
| app_id = |
| webapps::WebappsClient::Get()->GetAppIdForWebContents(web_contents); |
| } |
| // TODO(crbug.com/358168777): Possibly look up the app_id from the url |
| // for the notification use-case, if the notification system doesn't have the |
| // app_id to provide to this system. |
| |
| double score = GetScore(url); |
| for (SiteEngagementObserver& observer : observer_list_) |
| observer.OnEngagementEvent(web_contents, url, score, old_score, type, |
| app_id); |
| } |
| |
| bool SiteEngagementService::IsLastEngagementStale() const { |
| // |last_engagement_time| will be null when no engagement has been recorded |
| // (first run or post clearing site data), or if we are running in incognito. |
| // Do not regard these cases as stale. |
| base::Time last_engagement_time = GetLastEngagementTime(); |
| if (last_engagement_time.is_null()) |
| return false; |
| |
| // Stale is either too *far* back, or any amount *forward* in time. This could |
| // occur due to a changed clock, or extended non-use of the browser. |
| base::Time now = clock_->Now(); |
| return (now - last_engagement_time) >= GetStalePeriod() || |
| (now < last_engagement_time); |
| } |
| |
| SiteEngagementScore SiteEngagementService::CreateEngagementScore( |
| const GURL& origin) const { |
| // If we are in incognito, |settings| will automatically have the data from |
| // the original profile migrated in, so all engagement scores in incognito |
| // will be initialised to the values from the original profile. |
| return CreateEngagementScoreImpl( |
| clock_, origin, |
| permissions::PermissionsClient::Get()->GetSettingsMap(browser_context_)); |
| } |
| |
| } // namespace site_engagement |