1 // Copyright 2016 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_score.h"
6 
7 #include <algorithm>
8 #include <cmath>
9 #include <utility>
10 
11 #include "base/no_destructor.h"
12 #include "base/strings/string_number_conversions.h"
13 #include "base/time/clock.h"
14 #include "base/time/time.h"
15 #include "base/values.h"
16 #include "chrome/browser/content_settings/host_content_settings_map_factory.h"
17 #include "chrome/browser/engagement/site_engagement_metrics.h"
18 #include "components/content_settings/core/browser/host_content_settings_map.h"
19 #include "components/content_settings/core/common/content_settings.h"
20 #include "components/content_settings/core/common/content_settings_types.h"
21 #include "components/variations/variations_associated_data.h"
22 
23 namespace {
24 
25 // Delta within which to consider scores equal.
26 const double kScoreDelta = 0.001;
27 
28 // Delta within which to consider internal time values equal. Internal time
29 // values are in microseconds, so this delta comes out at one second.
30 const double kTimeDelta = 1000000;
31 
32 // Number of days after the last launch of an origin from an installed shortcut
33 // for which WEB_APP_INSTALLED_POINTS will be added to the engagement score.
34 const int kMaxDaysSinceShortcutLaunch = 10;
35 
DoublesConsideredDifferent(double value1,double value2,double delta)36 bool DoublesConsideredDifferent(double value1, double value2, double delta) {
37   double abs_difference = fabs(value1 - value2);
38   return abs_difference > delta;
39 }
40 
GetSiteEngagementScoreDictForSettings(const HostContentSettingsMap * settings,const GURL & origin_url)41 std::unique_ptr<base::DictionaryValue> GetSiteEngagementScoreDictForSettings(
42     const HostContentSettingsMap* settings,
43     const GURL& origin_url) {
44   if (!settings)
45     return std::make_unique<base::DictionaryValue>();
46 
47   std::unique_ptr<base::DictionaryValue> value =
48       base::DictionaryValue::From(settings->GetWebsiteSetting(
49           origin_url, origin_url, ContentSettingsType::SITE_ENGAGEMENT, NULL));
50 
51   if (value.get())
52     return value;
53 
54   return std::make_unique<base::DictionaryValue>();
55 }
56 
57 }  // namespace
58 
59 const double SiteEngagementScore::kMaxPoints = 100;
60 
61 const char SiteEngagementScore::kRawScoreKey[] = "rawScore";
62 const char SiteEngagementScore::kPointsAddedTodayKey[] = "pointsAddedToday";
63 const char SiteEngagementScore::kLastEngagementTimeKey[] = "lastEngagementTime";
64 const char SiteEngagementScore::kLastShortcutLaunchTimeKey[] =
65     "lastShortcutLaunchTime";
66 
67 // static
GetParamValues()68 SiteEngagementScore::ParamValues& SiteEngagementScore::GetParamValues() {
69   static base::NoDestructor<ParamValues> param_values([]() {
70     SiteEngagementScore::ParamValues param_values;
71     param_values[MAX_POINTS_PER_DAY] = {"max_points_per_day", 15};
72     param_values[DECAY_PERIOD_IN_HOURS] = {"decay_period_in_hours", 2};
73     param_values[DECAY_POINTS] = {"decay_points", 0};
74     param_values[DECAY_PROPORTION] = {"decay_proportion", 0.984};
75     param_values[SCORE_CLEANUP_THRESHOLD] = {"score_cleanup_threshold", 0.5};
76     param_values[NAVIGATION_POINTS] = {"navigation_points", 1.5};
77     param_values[USER_INPUT_POINTS] = {"user_input_points", 0.6};
78     param_values[VISIBLE_MEDIA_POINTS] = {"visible_media_playing_points", 0.06};
79     param_values[HIDDEN_MEDIA_POINTS] = {"hidden_media_playing_points", 0.01};
80     param_values[WEB_APP_INSTALLED_POINTS] = {"web_app_installed_points", 5};
81     param_values[FIRST_DAILY_ENGAGEMENT] = {"first_daily_engagement_points",
82                                             1.5};
83     param_values[BOOTSTRAP_POINTS] = {"bootstrap_points", 24};
84     param_values[MEDIUM_ENGAGEMENT_BOUNDARY] = {"medium_engagement_boundary",
85                                                 15};
86     param_values[HIGH_ENGAGEMENT_BOUNDARY] = {"high_engagement_boundary", 50};
87     param_values[MAX_DECAYS_PER_SCORE] = {"max_decays_per_score", 4};
88     param_values[LAST_ENGAGEMENT_GRACE_PERIOD_IN_HOURS] = {
89         "last_engagement_grace_period_in_hours", 1};
90     param_values[NOTIFICATION_INTERACTION_POINTS] = {
91         "notification_interaction_points", 1};
92     return param_values;
93   }());
94   return *param_values;
95 }
96 
GetMaxPointsPerDay()97 double SiteEngagementScore::GetMaxPointsPerDay() {
98   return GetParamValues()[MAX_POINTS_PER_DAY].second;
99 }
100 
GetDecayPeriodInHours()101 double SiteEngagementScore::GetDecayPeriodInHours() {
102   return GetParamValues()[DECAY_PERIOD_IN_HOURS].second;
103 }
104 
GetDecayPoints()105 double SiteEngagementScore::GetDecayPoints() {
106   return GetParamValues()[DECAY_POINTS].second;
107 }
108 
GetDecayProportion()109 double SiteEngagementScore::GetDecayProportion() {
110   return GetParamValues()[DECAY_PROPORTION].second;
111 }
112 
GetScoreCleanupThreshold()113 double SiteEngagementScore::GetScoreCleanupThreshold() {
114   return GetParamValues()[SCORE_CLEANUP_THRESHOLD].second;
115 }
116 
GetNavigationPoints()117 double SiteEngagementScore::GetNavigationPoints() {
118   return GetParamValues()[NAVIGATION_POINTS].second;
119 }
120 
GetUserInputPoints()121 double SiteEngagementScore::GetUserInputPoints() {
122   return GetParamValues()[USER_INPUT_POINTS].second;
123 }
124 
GetVisibleMediaPoints()125 double SiteEngagementScore::GetVisibleMediaPoints() {
126   return GetParamValues()[VISIBLE_MEDIA_POINTS].second;
127 }
128 
GetHiddenMediaPoints()129 double SiteEngagementScore::GetHiddenMediaPoints() {
130   return GetParamValues()[HIDDEN_MEDIA_POINTS].second;
131 }
132 
GetWebAppInstalledPoints()133 double SiteEngagementScore::GetWebAppInstalledPoints() {
134   return GetParamValues()[WEB_APP_INSTALLED_POINTS].second;
135 }
136 
GetFirstDailyEngagementPoints()137 double SiteEngagementScore::GetFirstDailyEngagementPoints() {
138   return GetParamValues()[FIRST_DAILY_ENGAGEMENT].second;
139 }
140 
GetBootstrapPoints()141 double SiteEngagementScore::GetBootstrapPoints() {
142   return GetParamValues()[BOOTSTRAP_POINTS].second;
143 }
144 
GetMediumEngagementBoundary()145 double SiteEngagementScore::GetMediumEngagementBoundary() {
146   return GetParamValues()[MEDIUM_ENGAGEMENT_BOUNDARY].second;
147 }
148 
GetHighEngagementBoundary()149 double SiteEngagementScore::GetHighEngagementBoundary() {
150   return GetParamValues()[HIGH_ENGAGEMENT_BOUNDARY].second;
151 }
152 
GetMaxDecaysPerScore()153 double SiteEngagementScore::GetMaxDecaysPerScore() {
154   return GetParamValues()[MAX_DECAYS_PER_SCORE].second;
155 }
156 
GetLastEngagementGracePeriodInHours()157 double SiteEngagementScore::GetLastEngagementGracePeriodInHours() {
158   return GetParamValues()[LAST_ENGAGEMENT_GRACE_PERIOD_IN_HOURS].second;
159 }
160 
GetNotificationInteractionPoints()161 double SiteEngagementScore::GetNotificationInteractionPoints() {
162   return GetParamValues()[NOTIFICATION_INTERACTION_POINTS].second;
163 }
164 
SetParamValuesForTesting()165 void SiteEngagementScore::SetParamValuesForTesting() {
166   GetParamValues()[MAX_POINTS_PER_DAY].second = 5;
167   GetParamValues()[DECAY_PERIOD_IN_HOURS].second = 7 * 24;
168   GetParamValues()[DECAY_POINTS].second = 5;
169   GetParamValues()[NAVIGATION_POINTS].second = 0.5;
170   GetParamValues()[USER_INPUT_POINTS].second = 0.05;
171   GetParamValues()[VISIBLE_MEDIA_POINTS].second = 0.02;
172   GetParamValues()[HIDDEN_MEDIA_POINTS].second = 0.01;
173   GetParamValues()[WEB_APP_INSTALLED_POINTS].second = 5;
174   GetParamValues()[BOOTSTRAP_POINTS].second = 8;
175   GetParamValues()[MEDIUM_ENGAGEMENT_BOUNDARY].second = 5;
176   GetParamValues()[HIGH_ENGAGEMENT_BOUNDARY].second = 50;
177   GetParamValues()[MAX_DECAYS_PER_SCORE].second = 1;
178   GetParamValues()[LAST_ENGAGEMENT_GRACE_PERIOD_IN_HOURS].second = 72;
179   GetParamValues()[NOTIFICATION_INTERACTION_POINTS].second = 1;
180 
181   // This is set to values that avoid interference with tests and are set when
182   // testing these features.
183   GetParamValues()[FIRST_DAILY_ENGAGEMENT].second = 0;
184   GetParamValues()[DECAY_PROPORTION].second = 1;
185   GetParamValues()[SCORE_CLEANUP_THRESHOLD].second = 0;
186 }
187 // static
UpdateFromVariations(const char * param_name)188 void SiteEngagementScore::UpdateFromVariations(const char* param_name) {
189   double param_vals[MAX_VARIATION];
190 
191   for (int i = 0; i < MAX_VARIATION; ++i) {
192     std::string param_string = variations::GetVariationParamValue(
193         param_name, GetParamValues()[i].first);
194 
195     // Bail out if we didn't get a param string for the key, or if we couldn't
196     // convert the param string to a double, or if we get a negative value.
197     if (param_string.empty() ||
198         !base::StringToDouble(param_string, &param_vals[i]) ||
199         param_vals[i] < 0) {
200       return;
201     }
202   }
203 
204   // Once we're sure everything is valid, assign the variation to the param
205   // values array.
206   for (int i = 0; i < MAX_VARIATION; ++i)
207     SiteEngagementScore::GetParamValues()[i].second = param_vals[i];
208 }
209 
SiteEngagementScore(base::Clock * clock,const GURL & origin,HostContentSettingsMap * settings)210 SiteEngagementScore::SiteEngagementScore(base::Clock* clock,
211                                          const GURL& origin,
212                                          HostContentSettingsMap* settings)
213     : SiteEngagementScore(
214           clock,
215           origin,
216           GetSiteEngagementScoreDictForSettings(settings, origin)) {
217   settings_map_ = settings;
218 }
219 
220 SiteEngagementScore::SiteEngagementScore(SiteEngagementScore&& other) = default;
221 
~SiteEngagementScore()222 SiteEngagementScore::~SiteEngagementScore() {}
223 
224 SiteEngagementScore& SiteEngagementScore::operator=(
225     SiteEngagementScore&& other) = default;
226 
AddPoints(double points)227 void SiteEngagementScore::AddPoints(double points) {
228   DCHECK_NE(0, points);
229 
230   // As the score is about to be updated, commit any decay that has happened
231   // since the last update.
232   raw_score_ = DecayedScore();
233 
234   base::Time now = clock_->Now();
235   if (!last_engagement_time_.is_null() &&
236       now.LocalMidnight() != last_engagement_time_.LocalMidnight()) {
237     points_added_today_ = 0;
238   }
239 
240   if (points_added_today_ == 0) {
241     // Award bonus engagement for the first engagement of the day for a site.
242     points += GetFirstDailyEngagementPoints();
243     SiteEngagementMetrics::RecordEngagement(
244         SiteEngagementService::ENGAGEMENT_FIRST_DAILY_ENGAGEMENT);
245   }
246 
247   double to_add = std::min(kMaxPoints - raw_score_,
248                            GetMaxPointsPerDay() - points_added_today_);
249   to_add = std::min(to_add, points);
250 
251   points_added_today_ += to_add;
252   raw_score_ += to_add;
253 
254   last_engagement_time_ = now;
255 }
256 
GetTotalScore() const257 double SiteEngagementScore::GetTotalScore() const {
258   return std::min(DecayedScore() + BonusIfShortcutLaunched(), kMaxPoints);
259 }
260 
GetDetails() const261 mojom::SiteEngagementDetails SiteEngagementScore::GetDetails() const {
262   mojom::SiteEngagementDetails engagement;
263   engagement.origin = origin_;
264   engagement.base_score = DecayedScore();
265   engagement.installed_bonus = BonusIfShortcutLaunched();
266   engagement.total_score = GetTotalScore();
267   return engagement;
268 }
269 
Commit()270 void SiteEngagementScore::Commit() {
271   DCHECK(settings_map_);
272   if (!UpdateScoreDict(score_dict_.get()))
273     return;
274 
275   settings_map_->SetWebsiteSettingDefaultScope(
276       origin_, GURL(), ContentSettingsType::SITE_ENGAGEMENT,
277       std::move(score_dict_));
278 }
279 
GetEngagementLevel() const280 blink::mojom::EngagementLevel SiteEngagementScore::GetEngagementLevel() const {
281   DCHECK_LT(GetMediumEngagementBoundary(), GetHighEngagementBoundary());
282 
283   double score = GetTotalScore();
284   if (score == 0)
285     return blink::mojom::EngagementLevel::NONE;
286 
287   if (score < 1)
288     return blink::mojom::EngagementLevel::MINIMAL;
289 
290   if (score < GetMediumEngagementBoundary())
291     return blink::mojom::EngagementLevel::LOW;
292 
293   if (score < GetHighEngagementBoundary())
294     return blink::mojom::EngagementLevel::MEDIUM;
295 
296   if (score < SiteEngagementScore::kMaxPoints)
297     return blink::mojom::EngagementLevel::HIGH;
298 
299   return blink::mojom::EngagementLevel::MAX;
300 }
301 
MaxPointsPerDayAdded() const302 bool SiteEngagementScore::MaxPointsPerDayAdded() const {
303   if (!last_engagement_time_.is_null() &&
304       clock_->Now().LocalMidnight() != last_engagement_time_.LocalMidnight()) {
305     return false;
306   }
307 
308   return points_added_today_ == GetMaxPointsPerDay();
309 }
310 
Reset(double points,const base::Time last_engagement_time)311 void SiteEngagementScore::Reset(double points,
312                                 const base::Time last_engagement_time) {
313   raw_score_ = points;
314   points_added_today_ = 0;
315 
316   // This must be set in order to prevent the score from decaying when read.
317   last_engagement_time_ = last_engagement_time;
318 }
319 
UpdateScoreDict(base::DictionaryValue * score_dict)320 bool SiteEngagementScore::UpdateScoreDict(base::DictionaryValue* score_dict) {
321   double raw_score_orig = 0;
322   double points_added_today_orig = 0;
323   double last_engagement_time_internal_orig = 0;
324   double last_shortcut_launch_time_internal_orig = 0;
325 
326   score_dict->GetDouble(kRawScoreKey, &raw_score_orig);
327   score_dict->GetDouble(kPointsAddedTodayKey, &points_added_today_orig);
328   score_dict->GetDouble(kLastEngagementTimeKey,
329                         &last_engagement_time_internal_orig);
330   score_dict->GetDouble(kLastShortcutLaunchTimeKey,
331                         &last_shortcut_launch_time_internal_orig);
332   bool changed =
333       DoublesConsideredDifferent(raw_score_orig, raw_score_, kScoreDelta) ||
334       DoublesConsideredDifferent(points_added_today_orig, points_added_today_,
335                                  kScoreDelta) ||
336       DoublesConsideredDifferent(last_engagement_time_internal_orig,
337                                  last_engagement_time_.ToInternalValue(),
338                                  kTimeDelta) ||
339       DoublesConsideredDifferent(last_shortcut_launch_time_internal_orig,
340                                  last_shortcut_launch_time_.ToInternalValue(),
341                                  kTimeDelta);
342 
343   if (!changed)
344     return false;
345 
346   score_dict->SetDouble(kRawScoreKey, raw_score_);
347   score_dict->SetDouble(kPointsAddedTodayKey, points_added_today_);
348   score_dict->SetDouble(kLastEngagementTimeKey,
349                         last_engagement_time_.ToInternalValue());
350   score_dict->SetDouble(kLastShortcutLaunchTimeKey,
351                         last_shortcut_launch_time_.ToInternalValue());
352 
353   return true;
354 }
355 
SiteEngagementScore(base::Clock * clock,const GURL & origin,std::unique_ptr<base::DictionaryValue> score_dict)356 SiteEngagementScore::SiteEngagementScore(
357     base::Clock* clock,
358     const GURL& origin,
359     std::unique_ptr<base::DictionaryValue> score_dict)
360     : clock_(clock),
361       raw_score_(0),
362       points_added_today_(0),
363       last_engagement_time_(),
364       last_shortcut_launch_time_(),
365       score_dict_(score_dict.release()),
366       origin_(origin),
367       settings_map_(nullptr) {
368   if (!score_dict_)
369     return;
370 
371   score_dict_->GetDouble(kRawScoreKey, &raw_score_);
372   score_dict_->GetDouble(kPointsAddedTodayKey, &points_added_today_);
373 
374   double internal_time;
375   if (score_dict_->GetDouble(kLastEngagementTimeKey, &internal_time))
376     last_engagement_time_ = base::Time::FromInternalValue(internal_time);
377   if (score_dict_->GetDouble(kLastShortcutLaunchTimeKey, &internal_time))
378     last_shortcut_launch_time_ = base::Time::FromInternalValue(internal_time);
379 }
380 
DecayedScore() const381 double SiteEngagementScore::DecayedScore() const {
382   // Note that users can change their clock, so from this system's perspective
383   // time can go backwards. If that does happen and the system detects that the
384   // current day is earlier than the last engagement, no decay (or growth) is
385   // applied.
386   int hours_since_engagement =
387       (clock_->Now() - last_engagement_time_).InHours();
388   if (hours_since_engagement < 0)
389     return raw_score_;
390 
391   int periods = hours_since_engagement / GetDecayPeriodInHours();
392   return std::max(0.0, raw_score_ * pow(GetDecayProportion(), periods) -
393                            periods * GetDecayPoints());
394 }
395 
BonusIfShortcutLaunched() const396 double SiteEngagementScore::BonusIfShortcutLaunched() const {
397   int days_since_shortcut_launch =
398       (clock_->Now() - last_shortcut_launch_time_).InDays();
399   if (days_since_shortcut_launch <= kMaxDaysSinceShortcutLaunch)
400     return GetWebAppInstalledPoints();
401   return 0;
402 }
403