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