1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/browser/engagement/site_engagement_service.h"
6
7 #include <stddef.h>
8
9 #include <algorithm>
10 #include <utility>
11
12 #include "base/bind.h"
13 #include "base/memory/scoped_refptr.h"
14 #include "base/metrics/field_trial.h"
15 #include "base/strings/string_util.h"
16 #include "base/task/thread_pool.h"
17 #include "base/time/clock.h"
18 #include "base/time/default_clock.h"
19 #include "base/time/time.h"
20 #include "base/values.h"
21 #include "chrome/browser/banners/app_banner_settings_helper.h"
22 #include "chrome/browser/content_settings/host_content_settings_map_factory.h"
23 #include "chrome/browser/engagement/site_engagement_helper.h"
24 #include "chrome/browser/engagement/site_engagement_metrics.h"
25 #include "chrome/browser/engagement/site_engagement_observer.h"
26 #include "chrome/browser/engagement/site_engagement_score.h"
27 #include "chrome/browser/engagement/site_engagement_service_factory.h"
28 #include "chrome/browser/history/history_service_factory.h"
29 #include "chrome/browser/profiles/profile.h"
30 #include "chrome/common/pref_names.h"
31 #include "components/browsing_data/core/browsing_data_utils.h"
32 #include "components/content_settings/core/browser/host_content_settings_map.h"
33 #include "components/content_settings/core/common/content_settings_pattern.h"
34 #include "components/history/core/browser/history_service.h"
35 #include "components/prefs/pref_service.h"
36 #include "content/public/browser/browser_task_traits.h"
37 #include "content/public/browser/browser_thread.h"
38 #include "content/public/browser/web_contents.h"
39 #include "url/gurl.h"
40
41 #if defined(OS_ANDROID)
42 #include "chrome/browser/engagement/site_engagement_service_android.h"
43 #endif
44
45 namespace {
46
47 const int FOUR_WEEKS_IN_DAYS = 28;
48
49 // Global bool to ensure we only update the parameters from variations once.
50 bool g_updated_from_variations = false;
51
52 // Length of time between metrics logging.
53 const int kMetricsIntervalInMinutes = 60;
54
55 // A clock that keeps showing the time it was constructed with.
56 class StoppedClock : public base::Clock {
57 public:
StoppedClock(base::Time time)58 explicit StoppedClock(base::Time time) : time_(time) {}
59 ~StoppedClock() override = default;
60
61 protected:
62 // base::Clock:
Now() const63 base::Time Now() const override { return time_; }
64
65 private:
66 const base::Time time_;
67
68 DISALLOW_COPY_AND_ASSIGN(StoppedClock);
69 };
70
71 // Helpers for fetching content settings for one type.
GetContentSettingsFromMap(HostContentSettingsMap * map,ContentSettingsType type)72 ContentSettingsForOneType GetContentSettingsFromMap(HostContentSettingsMap* map,
73 ContentSettingsType type) {
74 ContentSettingsForOneType content_settings;
75 map->GetSettingsForOneType(type, &content_settings);
76 return content_settings;
77 }
78
GetContentSettingsFromProfile(Profile * profile,ContentSettingsType type)79 ContentSettingsForOneType GetContentSettingsFromProfile(
80 Profile* profile,
81 ContentSettingsType type) {
82 return GetContentSettingsFromMap(
83 HostContentSettingsMapFactory::GetForProfile(profile), type);
84 }
85
86 // Returns the combined list of origins which either have site engagement
87 // data stored, or have other settings that would provide a score bonus.
GetEngagementOriginsFromContentSettings(HostContentSettingsMap * map)88 std::set<GURL> GetEngagementOriginsFromContentSettings(
89 HostContentSettingsMap* map) {
90 std::set<GURL> urls;
91
92 // Fetch URLs of sites with engagement details stored.
93 for (const auto& site :
94 GetContentSettingsFromMap(map, ContentSettingsType::SITE_ENGAGEMENT)) {
95 urls.insert(GURL(site.primary_pattern.ToString()));
96 }
97
98 return urls;
99 }
100
CreateEngagementScoreImpl(base::Clock * clock,const GURL & origin,HostContentSettingsMap * map)101 SiteEngagementScore CreateEngagementScoreImpl(base::Clock* clock,
102 const GURL& origin,
103 HostContentSettingsMap* map) {
104 return SiteEngagementScore(clock, origin, map);
105 }
106
GetDetailsImpl(base::Clock * clock,const GURL & origin,HostContentSettingsMap * map)107 mojom::SiteEngagementDetails GetDetailsImpl(base::Clock* clock,
108 const GURL& origin,
109 HostContentSettingsMap* map) {
110 return CreateEngagementScoreImpl(clock, origin, map).GetDetails();
111 }
112
GetAllDetailsImpl(browsing_data::TimePeriod time_period,base::Clock * clock,HostContentSettingsMap * map)113 std::vector<mojom::SiteEngagementDetails> GetAllDetailsImpl(
114 browsing_data::TimePeriod time_period,
115 base::Clock* clock,
116 HostContentSettingsMap* map) {
117 std::set<GURL> origins = GetEngagementOriginsFromContentSettings(map);
118
119 std::vector<mojom::SiteEngagementDetails> details;
120 details.reserve(origins.size());
121
122 auto begin_time = browsing_data::CalculateBeginDeleteTime(time_period);
123 auto end_time = browsing_data::CalculateEndDeleteTime(time_period);
124
125 for (const GURL& origin : origins) {
126 if (!origin.is_valid())
127 continue;
128
129 auto score = CreateEngagementScoreImpl(clock, origin, map);
130 auto last_engagement_time = score.last_engagement_time();
131 if (begin_time > last_engagement_time || end_time < last_engagement_time)
132 continue;
133
134 details.push_back(score.GetDetails());
135 }
136
137 return details;
138 }
139
140 // Only accept a navigation event for engagement if it is one of:
141 // a. direct typed navigation
142 // b. clicking on an omnibox suggestion brought up by typing a keyword
143 // c. clicking on a bookmark or opening a bookmark app
144 // d. a custom search engine keyword search (e.g. Wikipedia search box added as
145 // search engine)
146 // e. an automatically generated top level navigation (e.g. command line
147 // navigation, in product help link).
IsEngagementNavigation(ui::PageTransition transition)148 bool IsEngagementNavigation(ui::PageTransition transition) {
149 return ui::PageTransitionCoreTypeIs(transition, ui::PAGE_TRANSITION_TYPED) ||
150 ui::PageTransitionCoreTypeIs(transition,
151 ui::PAGE_TRANSITION_GENERATED) ||
152 ui::PageTransitionCoreTypeIs(transition,
153 ui::PAGE_TRANSITION_AUTO_BOOKMARK) ||
154 ui::PageTransitionCoreTypeIs(transition,
155 ui::PAGE_TRANSITION_KEYWORD_GENERATED) ||
156 ui::PageTransitionCoreTypeIs(transition,
157 ui::PAGE_TRANSITION_AUTO_TOPLEVEL);
158 }
159
160 } // namespace
161
162 const char SiteEngagementService::kEngagementParams[] = "SiteEngagement";
163
164 // static
Get(Profile * profile)165 SiteEngagementService* SiteEngagementService::Get(Profile* profile) {
166 return SiteEngagementServiceFactory::GetForProfile(profile);
167 }
168
169 // static
GetMaxPoints()170 double SiteEngagementService::GetMaxPoints() {
171 return SiteEngagementScore::kMaxPoints;
172 }
173
174 // static
IsEnabled()175 bool SiteEngagementService::IsEnabled() {
176 const std::string group_name =
177 base::FieldTrialList::FindFullName(kEngagementParams);
178 return !base::StartsWith(group_name, "Disabled",
179 base::CompareCase::SENSITIVE);
180 }
181
182 // static
GetScoreFromSettings(HostContentSettingsMap * settings,const GURL & origin)183 double SiteEngagementService::GetScoreFromSettings(
184 HostContentSettingsMap* settings,
185 const GURL& origin) {
186 return SiteEngagementScore(base::DefaultClock::GetInstance(), origin,
187 settings)
188 .GetTotalScore();
189 }
190
191 // static
192 std::vector<mojom::SiteEngagementDetails>
GetAllDetailsInBackground(base::Time now,scoped_refptr<HostContentSettingsMap> map)193 SiteEngagementService::GetAllDetailsInBackground(
194 base::Time now,
195 scoped_refptr<HostContentSettingsMap> map) {
196 StoppedClock clock(now);
197 return GetAllDetailsImpl(browsing_data::TimePeriod::ALL_TIME, &clock,
198 map.get());
199 }
200
SiteEngagementService(Profile * profile)201 SiteEngagementService::SiteEngagementService(Profile* profile)
202 : SiteEngagementService(profile, base::DefaultClock::GetInstance()) {
203 content::GetUIThreadTaskRunner({base::TaskPriority::BEST_EFFORT})
204 ->PostTask(FROM_HERE,
205 base::BindOnce(&SiteEngagementService::AfterStartupTask,
206 weak_factory_.GetWeakPtr()));
207
208 if (!g_updated_from_variations) {
209 SiteEngagementScore::UpdateFromVariations(kEngagementParams);
210 g_updated_from_variations = true;
211 }
212 }
213
~SiteEngagementService()214 SiteEngagementService::~SiteEngagementService() {
215 // Clear any observers to avoid dangling pointers back to this object.
216 for (auto& observer : observer_list_)
217 observer.Observe(nullptr);
218 }
219
Shutdown()220 void SiteEngagementService::Shutdown() {
221 history::HistoryService* history = HistoryServiceFactory::GetForProfile(
222 profile_, ServiceAccessType::IMPLICIT_ACCESS);
223 if (history)
224 history->RemoveObserver(this);
225 }
226
227 blink::mojom::EngagementLevel
GetEngagementLevel(const GURL & url) const228 SiteEngagementService::GetEngagementLevel(const GURL& url) const {
229 if (IsLastEngagementStale())
230 CleanupEngagementScores(true);
231
232 return CreateEngagementScore(url).GetEngagementLevel();
233 }
234
GetAllDetails() const235 std::vector<mojom::SiteEngagementDetails> SiteEngagementService::GetAllDetails()
236 const {
237 if (IsLastEngagementStale())
238 CleanupEngagementScores(true);
239
240 return GetAllDetailsImpl(
241 browsing_data::TimePeriod::ALL_TIME, clock_,
242 HostContentSettingsMapFactory::GetForProfile(profile_));
243 }
244
245 std::vector<mojom::SiteEngagementDetails>
GetAllDetailsEngagedInTimePeriod(browsing_data::TimePeriod time_period) const246 SiteEngagementService::GetAllDetailsEngagedInTimePeriod(
247 browsing_data::TimePeriod time_period) const {
248 if (IsLastEngagementStale())
249 CleanupEngagementScores(true);
250
251 return GetAllDetailsImpl(
252 time_period, clock_,
253 HostContentSettingsMapFactory::GetForProfile(profile_));
254 }
255
HandleNotificationInteraction(const GURL & url)256 void SiteEngagementService::HandleNotificationInteraction(const GURL& url) {
257 if (!ShouldRecordEngagement(url))
258 return;
259
260 AddPoints(url, SiteEngagementScore::GetNotificationInteractionPoints());
261
262 MaybeRecordMetrics();
263 OnEngagementEvent(nullptr /* web_contents */, url,
264 ENGAGEMENT_NOTIFICATION_INTERACTION);
265 }
266
IsBootstrapped() const267 bool SiteEngagementService::IsBootstrapped() const {
268 return GetTotalEngagementPoints() >=
269 SiteEngagementScore::GetBootstrapPoints();
270 }
271
IsEngagementAtLeast(const GURL & url,blink::mojom::EngagementLevel level) const272 bool SiteEngagementService::IsEngagementAtLeast(
273 const GURL& url,
274 blink::mojom::EngagementLevel level) const {
275 DCHECK_LT(SiteEngagementScore::GetMediumEngagementBoundary(),
276 SiteEngagementScore::GetHighEngagementBoundary());
277 double score = GetScore(url);
278 switch (level) {
279 case blink::mojom::EngagementLevel::NONE:
280 return true;
281 case blink::mojom::EngagementLevel::MINIMAL:
282 return score > 0;
283 case blink::mojom::EngagementLevel::LOW:
284 return score >= 1;
285 case blink::mojom::EngagementLevel::MEDIUM:
286 return score >= SiteEngagementScore::GetMediumEngagementBoundary();
287 case blink::mojom::EngagementLevel::HIGH:
288 return score >= SiteEngagementScore::GetHighEngagementBoundary();
289 case blink::mojom::EngagementLevel::MAX:
290 return score == SiteEngagementScore::kMaxPoints;
291 }
292 NOTREACHED();
293 return false;
294 }
295
AddObserver(SiteEngagementObserver * observer)296 void SiteEngagementService::AddObserver(SiteEngagementObserver* observer) {
297 observer_list_.AddObserver(observer);
298 }
299
RemoveObserver(SiteEngagementObserver * observer)300 void SiteEngagementService::RemoveObserver(SiteEngagementObserver* observer) {
301 observer_list_.RemoveObserver(observer);
302 }
303
ResetBaseScoreForURL(const GURL & url,double score)304 void SiteEngagementService::ResetBaseScoreForURL(const GURL& url,
305 double score) {
306 SiteEngagementScore engagement_score = CreateEngagementScore(url);
307 engagement_score.Reset(score, clock_->Now());
308 engagement_score.Commit();
309 }
310
SetLastShortcutLaunchTime(content::WebContents * web_contents,const GURL & url)311 void SiteEngagementService::SetLastShortcutLaunchTime(
312 content::WebContents* web_contents,
313 const GURL& url) {
314 SiteEngagementScore score = CreateEngagementScore(url);
315
316 // Record the number of days since the last launch in UMA. If the user's clock
317 // has changed back in time, set this to 0.
318 base::Time now = clock_->Now();
319 base::Time last_launch = score.last_shortcut_launch_time();
320 if (!last_launch.is_null()) {
321 SiteEngagementMetrics::RecordDaysSinceLastShortcutLaunch(
322 std::max(0, (now - last_launch).InDays()));
323 }
324
325 score.set_last_shortcut_launch_time(now);
326 score.Commit();
327
328 OnEngagementEvent(web_contents, url, ENGAGEMENT_WEBAPP_SHORTCUT_LAUNCH);
329 }
330
GetScore(const GURL & url) const331 double SiteEngagementService::GetScore(const GURL& url) const {
332 return GetDetails(url).total_score;
333 }
334
GetDetails(const GURL & url) const335 mojom::SiteEngagementDetails SiteEngagementService::GetDetails(
336 const GURL& url) const {
337 // Ensure that if engagement is stale, we clean things up before fetching the
338 // score.
339 if (IsLastEngagementStale())
340 CleanupEngagementScores(true);
341
342 return GetDetailsImpl(clock_, url,
343 HostContentSettingsMapFactory::GetForProfile(profile_));
344 }
345
GetTotalEngagementPoints() const346 double SiteEngagementService::GetTotalEngagementPoints() const {
347 std::vector<mojom::SiteEngagementDetails> details = GetAllDetails();
348
349 double total_score = 0;
350 for (const auto& detail : details)
351 total_score += detail.total_score;
352
353 return total_score;
354 }
355
AddPointsForTesting(const GURL & url,double points)356 void SiteEngagementService::AddPointsForTesting(const GURL& url,
357 double points) {
358 AddPoints(url, points);
359 }
360
361 #if defined(OS_ANDROID)
GetAndroidService() const362 SiteEngagementServiceAndroid* SiteEngagementService::GetAndroidService() const {
363 return android_service_.get();
364 }
365
SetAndroidService(std::unique_ptr<SiteEngagementServiceAndroid> android_service)366 void SiteEngagementService::SetAndroidService(
367 std::unique_ptr<SiteEngagementServiceAndroid> android_service) {
368 android_service_ = std::move(android_service);
369 }
370 #endif
371
SiteEngagementService(Profile * profile,base::Clock * clock)372 SiteEngagementService::SiteEngagementService(Profile* profile,
373 base::Clock* clock)
374 : profile_(profile), clock_(clock) {
375 // May be null in tests.
376 history::HistoryService* history = HistoryServiceFactory::GetForProfile(
377 profile, ServiceAccessType::IMPLICIT_ACCESS);
378 if (history)
379 history->AddObserver(this);
380 }
381
AddPoints(const GURL & url,double points)382 void SiteEngagementService::AddPoints(const GURL& url, double points) {
383 if (points == 0)
384 return;
385
386 // Trigger a cleanup and date adjustment if it has been a substantial length
387 // of time since *any* engagement was recorded by the service. This will
388 // ensure that we do not decay scores when the user did not use the browser.
389 if (IsLastEngagementStale())
390 CleanupEngagementScores(true);
391
392 SiteEngagementScore score = CreateEngagementScore(url);
393 score.AddPoints(points);
394 score.Commit();
395
396 SetLastEngagementTime(score.last_engagement_time());
397 }
398
AfterStartupTask()399 void SiteEngagementService::AfterStartupTask() {
400 // Check if we need to reset last engagement times on startup - we want to
401 // avoid doing this in AddPoints() if possible. It is still necessary to check
402 // in AddPoints for people who never restart Chrome, but leave it open and
403 // their computer on standby.
404 CleanupEngagementScores(IsLastEngagementStale());
405 }
406
CleanupEngagementScores(bool update_last_engagement_time) const407 void SiteEngagementService::CleanupEngagementScores(
408 bool update_last_engagement_time) const {
409 // We want to rebase last engagement times relative to MaxDecaysPerScore
410 // periods of decay in the past.
411 base::Time now = clock_->Now();
412 base::Time last_engagement_time = GetLastEngagementTime();
413 base::Time rebase_time = now - GetMaxDecayPeriod();
414 base::Time new_last_engagement_time;
415
416 // If |update_last_engagement_time| is true, we must have either:
417 // a) last_engagement_time is in the future; OR
418 // b) last_engagement_time < rebase_time < now
419 DCHECK(!update_last_engagement_time || last_engagement_time >= now ||
420 (last_engagement_time < rebase_time && rebase_time < now));
421
422 // Cap |last_engagement_time| at |now| if it is in the future. This ensures
423 // that we use sane offsets when a user has adjusted their clock backwards and
424 // have a mix of scores prior to and after |now|.
425 if (last_engagement_time > now)
426 last_engagement_time = now;
427
428 HostContentSettingsMap* settings_map =
429 HostContentSettingsMapFactory::GetForProfile(profile_);
430 for (const auto& site : GetContentSettingsFromProfile(
431 profile_, ContentSettingsType::SITE_ENGAGEMENT)) {
432 GURL origin(site.primary_pattern.ToString());
433
434 if (origin.is_valid()) {
435 SiteEngagementScore score = CreateEngagementScore(origin);
436 if (update_last_engagement_time) {
437 // Catch cases of users moving their clocks, or a potential race where
438 // a score content setting is written out to prefs, but the updated
439 // |last_engagement_time| was not written, as both are lossy
440 // preferences. |rebase_time| is strictly in the past, so any score with
441 // a last updated time in the future is caught by this branch.
442 if (score.last_engagement_time() > rebase_time) {
443 score.set_last_engagement_time(now);
444 } else if (score.last_engagement_time() > last_engagement_time) {
445 // This score is newer than |last_engagement_time|, but older than
446 // |rebase_time|. It should still be rebased with no offset as we
447 // don't accurately know what the offset should be.
448 score.set_last_engagement_time(rebase_time);
449 } else {
450 // Work out the offset between this score's last engagement time and
451 // the last time the service recorded any engagement. Set the score's
452 // last engagement time to rebase_time - offset to preserve its state,
453 // relative to the rebase date. This ensures that the score will decay
454 // the next time it is used, but will not decay too much.
455 base::TimeDelta offset =
456 last_engagement_time - score.last_engagement_time();
457 base::Time rebase_score_time = rebase_time - offset;
458 score.set_last_engagement_time(rebase_score_time);
459 }
460
461 if (score.last_engagement_time() > new_last_engagement_time)
462 new_last_engagement_time = score.last_engagement_time();
463 score.Commit();
464 }
465
466 if (score.GetTotalScore() >
467 SiteEngagementScore::GetScoreCleanupThreshold())
468 continue;
469 }
470
471 // This origin has a score of 0. Wipe it from content settings.
472 settings_map->SetWebsiteSettingDefaultScope(
473 origin, GURL(), ContentSettingsType::SITE_ENGAGEMENT, nullptr);
474 }
475
476 // Set the last engagement time to be consistent with the scores. This will
477 // only occur if |update_last_engagement_time| is true.
478 if (!new_last_engagement_time.is_null())
479 SetLastEngagementTime(new_last_engagement_time);
480 }
481
MaybeRecordMetrics()482 void SiteEngagementService::MaybeRecordMetrics() {
483 base::Time now = clock_->Now();
484 if (profile_->IsOffTheRecord() ||
485 (!last_metrics_time_.is_null() &&
486 (now - last_metrics_time_).InMinutes() < kMetricsIntervalInMinutes)) {
487 return;
488 }
489
490 // Clean up engagement first before retrieving scores.
491 if (IsLastEngagementStale())
492 CleanupEngagementScores(true);
493
494 last_metrics_time_ = now;
495
496 // Retrieve details on a background thread as this is expensive. We may end up
497 // with minor data inconsistency but this doesn't really matter for metrics
498 // purposes.
499 //
500 // The profile and its KeyedServices are normally destroyed before the
501 // ThreadPool shuts down background threads, so the task needs to hold a
502 // strong reference to HostContentSettingsMap (which supports outliving the
503 // profile), and needs to avoid using any members of SiteEngagementService
504 // (which does not). See https://crbug.com/900022.
505 base::ThreadPool::PostTaskAndReplyWithResult(
506 FROM_HERE,
507 {base::TaskPriority::BEST_EFFORT,
508 base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
509 base::BindOnce(
510 &GetAllDetailsInBackground, now,
511 base::WrapRefCounted(
512 HostContentSettingsMapFactory::GetForProfile(profile_))),
513 base::BindOnce(&SiteEngagementService::RecordMetrics,
514 weak_factory_.GetWeakPtr()));
515 }
516
RecordMetrics(std::vector<mojom::SiteEngagementDetails> details)517 void SiteEngagementService::RecordMetrics(
518 std::vector<mojom::SiteEngagementDetails> details) {
519 std::sort(details.begin(), details.end(),
520 [](const mojom::SiteEngagementDetails& lhs,
521 const mojom::SiteEngagementDetails& rhs) {
522 return lhs.total_score < rhs.total_score;
523 });
524
525 int total_origins = details.size();
526
527 double total_engagement = 0;
528 int origins_with_max_engagement = 0;
529 for (const auto& detail : details) {
530 if (detail.total_score == SiteEngagementScore::kMaxPoints)
531 ++origins_with_max_engagement;
532 total_engagement += detail.total_score;
533 }
534
535 double mean_engagement =
536 (total_origins == 0 ? 0 : total_engagement / total_origins);
537
538 SiteEngagementMetrics::RecordTotalOriginsEngaged(total_origins);
539 SiteEngagementMetrics::RecordTotalSiteEngagement(total_engagement);
540 SiteEngagementMetrics::RecordMeanEngagement(mean_engagement);
541 SiteEngagementMetrics::RecordMedianEngagement(
542 GetMedianEngagementFromSortedDetails(details));
543 SiteEngagementMetrics::RecordEngagementScores(details);
544
545 SiteEngagementMetrics::RecordOriginsWithMaxDailyEngagement(
546 OriginsWithMaxDailyEngagement());
547 SiteEngagementMetrics::RecordOriginsWithMaxEngagement(
548 origins_with_max_engagement);
549 }
550
ShouldRecordEngagement(const GURL & url) const551 bool SiteEngagementService::ShouldRecordEngagement(const GURL& url) const {
552 return url.SchemeIsHTTPOrHTTPS();
553 }
554
GetLastEngagementTime() const555 base::Time SiteEngagementService::GetLastEngagementTime() const {
556 if (profile_->IsOffTheRecord())
557 return base::Time();
558
559 return base::Time::FromInternalValue(
560 profile_->GetPrefs()->GetInt64(prefs::kSiteEngagementLastUpdateTime));
561 }
562
SetLastEngagementTime(base::Time last_engagement_time) const563 void SiteEngagementService::SetLastEngagementTime(
564 base::Time last_engagement_time) const {
565 if (profile_->IsOffTheRecord())
566 return;
567 profile_->GetPrefs()->SetInt64(prefs::kSiteEngagementLastUpdateTime,
568 last_engagement_time.ToInternalValue());
569 }
570
GetMaxDecayPeriod() const571 base::TimeDelta SiteEngagementService::GetMaxDecayPeriod() const {
572 return base::TimeDelta::FromHours(
573 SiteEngagementScore::GetDecayPeriodInHours()) *
574 SiteEngagementScore::GetMaxDecaysPerScore();
575 }
576
GetStalePeriod() const577 base::TimeDelta SiteEngagementService::GetStalePeriod() const {
578 return GetMaxDecayPeriod() +
579 base::TimeDelta::FromHours(
580 SiteEngagementScore::GetLastEngagementGracePeriodInHours());
581 }
582
GetMedianEngagementFromSortedDetails(const std::vector<mojom::SiteEngagementDetails> & details) const583 double SiteEngagementService::GetMedianEngagementFromSortedDetails(
584 const std::vector<mojom::SiteEngagementDetails>& details) const {
585 if (details.empty())
586 return 0;
587
588 // Calculate the median as the middle value of the sorted engagement scores
589 // if there are an odd number of scores, or the average of the two middle
590 // scores otherwise.
591 size_t mid = details.size() / 2;
592 if (details.size() % 2 == 1)
593 return details[mid].total_score;
594 else
595 return (details[mid - 1].total_score + details[mid].total_score) / 2;
596 }
597
HandleMediaPlaying(content::WebContents * web_contents,bool is_hidden)598 void SiteEngagementService::HandleMediaPlaying(
599 content::WebContents* web_contents,
600 bool is_hidden) {
601 const GURL& url = web_contents->GetLastCommittedURL();
602 if (!ShouldRecordEngagement(url))
603 return;
604
605 AddPoints(url, is_hidden ? SiteEngagementScore::GetHiddenMediaPoints()
606 : SiteEngagementScore::GetVisibleMediaPoints());
607
608 MaybeRecordMetrics();
609 OnEngagementEvent(
610 web_contents, url,
611 is_hidden ? ENGAGEMENT_MEDIA_HIDDEN : ENGAGEMENT_MEDIA_VISIBLE);
612 }
613
HandleNavigation(content::WebContents * web_contents,ui::PageTransition transition)614 void SiteEngagementService::HandleNavigation(content::WebContents* web_contents,
615 ui::PageTransition transition) {
616 const GURL& url = web_contents->GetLastCommittedURL();
617 if (!IsEngagementNavigation(transition) || !ShouldRecordEngagement(url))
618 return;
619
620 AddPoints(url, SiteEngagementScore::GetNavigationPoints());
621
622 MaybeRecordMetrics();
623 OnEngagementEvent(web_contents, url, ENGAGEMENT_NAVIGATION);
624 }
625
HandleUserInput(content::WebContents * web_contents,EngagementType type)626 void SiteEngagementService::HandleUserInput(content::WebContents* web_contents,
627 EngagementType type) {
628 const GURL& url = web_contents->GetLastCommittedURL();
629 if (!ShouldRecordEngagement(url))
630 return;
631
632 AddPoints(url, SiteEngagementScore::GetUserInputPoints());
633
634 MaybeRecordMetrics();
635 OnEngagementEvent(web_contents, url, type);
636 }
637
OnEngagementEvent(content::WebContents * web_contents,const GURL & url,EngagementType type)638 void SiteEngagementService::OnEngagementEvent(
639 content::WebContents* web_contents,
640 const GURL& url,
641 EngagementType type) {
642 SiteEngagementMetrics::RecordEngagement(type);
643
644 double score = GetScore(url);
645 for (SiteEngagementObserver& observer : observer_list_)
646 observer.OnEngagementEvent(web_contents, url, score, type);
647 }
648
IsLastEngagementStale() const649 bool SiteEngagementService::IsLastEngagementStale() const {
650 // |last_engagement_time| will be null when no engagement has been recorded
651 // (first run or post clearing site data), or if we are running in incognito.
652 // Do not regard these cases as stale.
653 base::Time last_engagement_time = GetLastEngagementTime();
654 if (last_engagement_time.is_null())
655 return false;
656
657 // Stale is either too *far* back, or any amount *forward* in time. This could
658 // occur due to a changed clock, or extended non-use of the browser.
659 base::Time now = clock_->Now();
660 return (now - last_engagement_time) >= GetStalePeriod() ||
661 (now < last_engagement_time);
662 }
663
OnURLsDeleted(history::HistoryService * history_service,const history::DeletionInfo & deletion_info)664 void SiteEngagementService::OnURLsDeleted(
665 history::HistoryService* history_service,
666 const history::DeletionInfo& deletion_info) {
667 std::multiset<GURL> origins;
668 for (const history::URLRow& row : deletion_info.deleted_rows())
669 origins.insert(row.url().GetOrigin());
670
671 UpdateEngagementScores(origins, deletion_info.is_from_expiration(),
672 deletion_info.deleted_urls_origin_map());
673 }
674
CreateEngagementScore(const GURL & origin) const675 SiteEngagementScore SiteEngagementService::CreateEngagementScore(
676 const GURL& origin) const {
677 // If we are in incognito, |settings| will automatically have the data from
678 // the original profile migrated in, so all engagement scores in incognito
679 // will be initialised to the values from the original profile.
680 return CreateEngagementScoreImpl(
681 clock_, origin, HostContentSettingsMapFactory::GetForProfile(profile_));
682 }
683
OriginsWithMaxDailyEngagement() const684 int SiteEngagementService::OriginsWithMaxDailyEngagement() const {
685 int total_origins = 0;
686
687 // We cannot call GetScoreMap as we need the score objects, not raw scores.
688 for (const auto& site : GetContentSettingsFromProfile(
689 profile_, ContentSettingsType::SITE_ENGAGEMENT)) {
690 GURL origin(site.primary_pattern.ToString());
691
692 if (!origin.is_valid())
693 continue;
694
695 if (CreateEngagementScore(origin).MaxPointsPerDayAdded())
696 ++total_origins;
697 }
698
699 return total_origins;
700 }
701
UpdateEngagementScores(const std::multiset<GURL> & deleted_origins,bool expired,const history::OriginCountAndLastVisitMap & remaining_origins)702 void SiteEngagementService::UpdateEngagementScores(
703 const std::multiset<GURL>& deleted_origins,
704 bool expired,
705 const history::OriginCountAndLastVisitMap& remaining_origins) {
706 // The most in-the-past option in the Clear Browsing Dialog aside from "all
707 // time" is 4 weeks ago. Set the last updated date to 4 weeks ago for origins
708 // where we can't find a valid last visit date.
709 base::Time now = clock_->Now();
710 base::Time four_weeks_ago =
711 now - base::TimeDelta::FromDays(FOUR_WEEKS_IN_DAYS);
712
713 HostContentSettingsMap* settings_map =
714 HostContentSettingsMapFactory::GetForProfile(profile_);
715
716 for (const auto& origin_to_count : remaining_origins) {
717 GURL origin = origin_to_count.first;
718 // It appears that the history service occasionally sends bad URLs to us.
719 // See crbug.com/612881.
720 if (!origin.is_valid())
721 continue;
722
723 int remaining = origin_to_count.second.first;
724 base::Time last_visit = origin_to_count.second.second;
725 int deleted = deleted_origins.count(origin);
726
727 // Do not update engagement scores if the deletion was an expiry, but the
728 // URL still has entries in history.
729 if ((expired && remaining != 0) || deleted == 0)
730 continue;
731
732 // Remove origins that have no urls left.
733 if (remaining == 0) {
734 settings_map->SetWebsiteSettingDefaultScope(
735 origin, GURL(), ContentSettingsType::SITE_ENGAGEMENT, nullptr);
736 continue;
737 }
738
739 // Remove engagement proportional to the urls expired from the origin's
740 // entire history.
741 double proportion_remaining =
742 static_cast<double>(remaining) / (remaining + deleted);
743 if (last_visit.is_null() || last_visit > now)
744 last_visit = four_weeks_ago;
745
746 // At this point, we are going to proportionally decay the origin's
747 // engagement, and reset its last visit date to the last visit to a URL
748 // under the origin in history. If this new last visit date is long enough
749 // in the past, the next time the origin's engagement is accessed the
750 // automatic decay will kick in - i.e. a double decay will have occurred.
751 // To prevent this, compute the decay that would have taken place since the
752 // new last visit and add it to the engagement at this point. When the
753 // engagement is next accessed, it will decay back to the proportionally
754 // reduced value rather than being decayed once here, and then once again
755 // when it is next accessed.
756 // TODO(703848): Move the proportional decay logic into SiteEngagementScore,
757 // so it can decay raw_score_ directly, without the double-decay issue.
758 SiteEngagementScore engagement_score = CreateEngagementScore(origin);
759
760 double new_score = proportion_remaining * engagement_score.GetTotalScore();
761 int hours_since_engagement = (now - last_visit).InHours();
762 int periods =
763 hours_since_engagement / SiteEngagementScore::GetDecayPeriodInHours();
764 new_score += periods * SiteEngagementScore::GetDecayPoints();
765 new_score *= pow(1.0 / SiteEngagementScore::GetDecayProportion(), periods);
766
767 double score = std::min(SiteEngagementScore::kMaxPoints, new_score);
768 engagement_score.Reset(score, last_visit);
769 if (!engagement_score.last_shortcut_launch_time().is_null() &&
770 engagement_score.last_shortcut_launch_time() > last_visit) {
771 engagement_score.set_last_shortcut_launch_time(last_visit);
772 }
773
774 engagement_score.Commit();
775 }
776
777 SetLastEngagementTime(now);
778 }
779