| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/engagement/site_engagement_score.h" |
| |
| #include <algorithm> |
| #include <cmath> |
| #include <utility> |
| |
| #include "base/memory/ptr_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/time/clock.h" |
| #include "base/time/time.h" |
| #include "base/values.h" |
| #include "chrome/browser/content_settings/host_content_settings_map_factory.h" |
| #include "chrome/browser/engagement/site_engagement_metrics.h" |
| #include "components/content_settings/core/browser/host_content_settings_map.h" |
| #include "components/content_settings/core/common/content_settings.h" |
| #include "components/content_settings/core/common/content_settings_types.h" |
| #include "components/variations/variations_associated_data.h" |
| |
| namespace { |
| |
| // Delta within which to consider scores equal. |
| const double kScoreDelta = 0.001; |
| |
| // Delta within which to consider internal time values equal. Internal time |
| // values are in microseconds, so this delta comes out at one second. |
| const double kTimeDelta = 1000000; |
| |
| // Number of days after the last launch of an origin from an installed shortcut |
| // for which WEB_APP_INSTALLED_POINTS will be added to the engagement score. |
| const int kMaxDaysSinceShortcutLaunch = 10; |
| |
| bool DoublesConsideredDifferent(double value1, double value2, double delta) { |
| double abs_difference = fabs(value1 - value2); |
| return abs_difference > delta; |
| } |
| |
| std::unique_ptr<base::DictionaryValue> GetScoreDictForSettings( |
| const HostContentSettingsMap* settings, |
| const GURL& origin_url) { |
| if (!settings) |
| return base::MakeUnique<base::DictionaryValue>(); |
| |
| std::unique_ptr<base::DictionaryValue> value = |
| base::DictionaryValue::From(settings->GetWebsiteSetting( |
| origin_url, origin_url, CONTENT_SETTINGS_TYPE_SITE_ENGAGEMENT, |
| content_settings::ResourceIdentifier(), NULL)); |
| |
| if (value.get()) |
| return value; |
| |
| return base::MakeUnique<base::DictionaryValue>(); |
| } |
| |
| } // namespace |
| |
| const double SiteEngagementScore::kMaxPoints = 100; |
| |
| const char SiteEngagementScore::kRawScoreKey[] = "rawScore"; |
| const char SiteEngagementScore::kPointsAddedTodayKey[] = "pointsAddedToday"; |
| const char SiteEngagementScore::kLastEngagementTimeKey[] = "lastEngagementTime"; |
| const char SiteEngagementScore::kLastShortcutLaunchTimeKey[] = |
| "lastShortcutLaunchTime"; |
| |
| // static |
| SiteEngagementScore::ParamValues& SiteEngagementScore::GetParamValues() { |
| CR_DEFINE_STATIC_LOCAL(ParamValues, param_values, (BuildParamValues())); |
| return param_values; |
| } |
| |
| // static |
| SiteEngagementScore::ParamValues SiteEngagementScore::BuildParamValues() { |
| SiteEngagementScore::ParamValues param_values; |
| param_values[MAX_POINTS_PER_DAY] = {"max_points_per_day", 15}; |
| param_values[DECAY_PERIOD_IN_HOURS] = {"decay_period_in_hours", 2}; |
| param_values[DECAY_POINTS] = {"decay_points", 0}; |
| param_values[DECAY_PROPORTION] = {"decay_proportion", 0.984}; |
| param_values[SCORE_CLEANUP_THRESHOLD] = {"score_cleanup_threshold", 0.5}; |
| param_values[NAVIGATION_POINTS] = {"navigation_points", 1.5}; |
| param_values[USER_INPUT_POINTS] = {"user_input_points", 0.6}; |
| param_values[VISIBLE_MEDIA_POINTS] = {"visible_media_playing_points", 0.06}; |
| param_values[HIDDEN_MEDIA_POINTS] = {"hidden_media_playing_points", 0.01}; |
| param_values[WEB_APP_INSTALLED_POINTS] = {"web_app_installed_points", 5}; |
| param_values[FIRST_DAILY_ENGAGEMENT] = {"first_daily_engagement_points", 1.5}; |
| param_values[BOOTSTRAP_POINTS] = {"bootstrap_points", 24}; |
| param_values[MEDIUM_ENGAGEMENT_BOUNDARY] = {"medium_engagement_boundary", 15}; |
| param_values[HIGH_ENGAGEMENT_BOUNDARY] = {"high_engagement_boundary", 50}; |
| param_values[MAX_DECAYS_PER_SCORE] = {"max_decays_per_score", 4}; |
| param_values[LAST_ENGAGEMENT_GRACE_PERIOD_IN_HOURS] = { |
| "last_engagement_grace_period_in_hours", 1}; |
| param_values[NOTIFICATION_PERMISSION_POINTS] = { |
| "notification_permission_points", 5}; |
| param_values[NOTIFICATION_INTERACTION_POINTS] = { |
| "notification_interaction_points", 1}; |
| return param_values; |
| } |
| |
| double SiteEngagementScore::GetMaxPointsPerDay() { |
| return GetParamValues()[MAX_POINTS_PER_DAY].second; |
| } |
| |
| double SiteEngagementScore::GetDecayPeriodInHours() { |
| return GetParamValues()[DECAY_PERIOD_IN_HOURS].second; |
| } |
| |
| double SiteEngagementScore::GetDecayPoints() { |
| return GetParamValues()[DECAY_POINTS].second; |
| } |
| |
| double SiteEngagementScore::GetDecayProportion() { |
| return GetParamValues()[DECAY_PROPORTION].second; |
| } |
| |
| double SiteEngagementScore::GetScoreCleanupThreshold() { |
| return GetParamValues()[SCORE_CLEANUP_THRESHOLD].second; |
| } |
| |
| double SiteEngagementScore::GetNavigationPoints() { |
| return GetParamValues()[NAVIGATION_POINTS].second; |
| } |
| |
| double SiteEngagementScore::GetUserInputPoints() { |
| return GetParamValues()[USER_INPUT_POINTS].second; |
| } |
| |
| double SiteEngagementScore::GetVisibleMediaPoints() { |
| return GetParamValues()[VISIBLE_MEDIA_POINTS].second; |
| } |
| |
| double SiteEngagementScore::GetHiddenMediaPoints() { |
| return GetParamValues()[HIDDEN_MEDIA_POINTS].second; |
| } |
| |
| double SiteEngagementScore::GetWebAppInstalledPoints() { |
| return GetParamValues()[WEB_APP_INSTALLED_POINTS].second; |
| } |
| |
| double SiteEngagementScore::GetFirstDailyEngagementPoints() { |
| return GetParamValues()[FIRST_DAILY_ENGAGEMENT].second; |
| } |
| |
| double SiteEngagementScore::GetBootstrapPoints() { |
| return GetParamValues()[BOOTSTRAP_POINTS].second; |
| } |
| |
| double SiteEngagementScore::GetMediumEngagementBoundary() { |
| return GetParamValues()[MEDIUM_ENGAGEMENT_BOUNDARY].second; |
| } |
| |
| double SiteEngagementScore::GetHighEngagementBoundary() { |
| return GetParamValues()[HIGH_ENGAGEMENT_BOUNDARY].second; |
| } |
| |
| double SiteEngagementScore::GetMaxDecaysPerScore() { |
| return GetParamValues()[MAX_DECAYS_PER_SCORE].second; |
| } |
| |
| double SiteEngagementScore::GetLastEngagementGracePeriodInHours() { |
| return GetParamValues()[LAST_ENGAGEMENT_GRACE_PERIOD_IN_HOURS].second; |
| } |
| |
| double SiteEngagementScore::GetNotificationPermissionPoints() { |
| return GetParamValues()[NOTIFICATION_PERMISSION_POINTS].second; |
| } |
| |
| double SiteEngagementScore::GetNotificationInteractionPoints() { |
| return GetParamValues()[NOTIFICATION_INTERACTION_POINTS].second; |
| } |
| |
| void SiteEngagementScore::SetParamValuesForTesting() { |
| GetParamValues()[MAX_POINTS_PER_DAY].second = 5; |
| GetParamValues()[DECAY_PERIOD_IN_HOURS].second = 7 * 24; |
| GetParamValues()[DECAY_POINTS].second = 5; |
| GetParamValues()[NAVIGATION_POINTS].second = 0.5; |
| GetParamValues()[USER_INPUT_POINTS].second = 0.05; |
| GetParamValues()[VISIBLE_MEDIA_POINTS].second = 0.02; |
| GetParamValues()[HIDDEN_MEDIA_POINTS].second = 0.01; |
| GetParamValues()[WEB_APP_INSTALLED_POINTS].second = 5; |
| GetParamValues()[BOOTSTRAP_POINTS].second = 8; |
| GetParamValues()[MEDIUM_ENGAGEMENT_BOUNDARY].second = 5; |
| GetParamValues()[HIGH_ENGAGEMENT_BOUNDARY].second = 50; |
| GetParamValues()[MAX_DECAYS_PER_SCORE].second = 1; |
| GetParamValues()[LAST_ENGAGEMENT_GRACE_PERIOD_IN_HOURS].second = 72; |
| GetParamValues()[NOTIFICATION_PERMISSION_POINTS].second = 5; |
| GetParamValues()[NOTIFICATION_INTERACTION_POINTS].second = 1; |
| |
| // This is set to values that avoid interference with tests and are set when |
| // testing these features. |
| GetParamValues()[FIRST_DAILY_ENGAGEMENT].second = 0; |
| GetParamValues()[DECAY_PROPORTION].second = 1; |
| GetParamValues()[SCORE_CLEANUP_THRESHOLD].second = 0; |
| } |
| // static |
| void SiteEngagementScore::UpdateFromVariations(const char* param_name) { |
| double param_vals[MAX_VARIATION]; |
| |
| for (int i = 0; i < MAX_VARIATION; ++i) { |
| std::string param_string = variations::GetVariationParamValue( |
| param_name, GetParamValues()[i].first); |
| |
| // Bail out if we didn't get a param string for the key, or if we couldn't |
| // convert the param string to a double, or if we get a negative value. |
| if (param_string.empty() || |
| !base::StringToDouble(param_string, ¶m_vals[i]) || |
| param_vals[i] < 0) { |
| return; |
| } |
| } |
| |
| // Once we're sure everything is valid, assign the variation to the param |
| // values array. |
| for (int i = 0; i < MAX_VARIATION; ++i) |
| SiteEngagementScore::GetParamValues()[i].second = param_vals[i]; |
| } |
| |
| SiteEngagementScore::SiteEngagementScore( |
| base::Clock* clock, |
| const GURL& origin, |
| HostContentSettingsMap* settings) |
| : SiteEngagementScore( |
| clock, |
| origin, |
| GetScoreDictForSettings(settings, origin)) { |
| settings_map_ = settings; |
| } |
| |
| SiteEngagementScore::SiteEngagementScore(SiteEngagementScore&& other) = default; |
| |
| SiteEngagementScore::~SiteEngagementScore() {} |
| |
| SiteEngagementScore& SiteEngagementScore::operator=( |
| SiteEngagementScore&& other) = default; |
| |
| void SiteEngagementScore::AddPoints(double points) { |
| DCHECK_NE(0, points); |
| double decayed_score = DecayedScore(); |
| |
| // Record the original and decayed scores after a decay event. |
| if (decayed_score < raw_score_) { |
| SiteEngagementMetrics::RecordScoreDecayedFrom(raw_score_); |
| SiteEngagementMetrics::RecordScoreDecayedTo(decayed_score); |
| } |
| |
| // As the score is about to be updated, commit any decay that has happened |
| // since the last update. |
| raw_score_ = decayed_score; |
| |
| base::Time now = clock_->Now(); |
| if (!last_engagement_time_.is_null() && |
| now.LocalMidnight() != last_engagement_time_.LocalMidnight()) { |
| points_added_today_ = 0; |
| } |
| |
| if (points_added_today_ == 0) { |
| // Award bonus engagement for the first engagement of the day for a site. |
| points += GetFirstDailyEngagementPoints(); |
| SiteEngagementMetrics::RecordEngagement( |
| SiteEngagementService::ENGAGEMENT_FIRST_DAILY_ENGAGEMENT); |
| } |
| |
| double to_add = std::min(kMaxPoints - raw_score_, |
| GetMaxPointsPerDay() - points_added_today_); |
| to_add = std::min(to_add, points); |
| |
| points_added_today_ += to_add; |
| raw_score_ += to_add; |
| |
| last_engagement_time_ = now; |
| } |
| |
| double SiteEngagementScore::GetTotalScore() const { |
| return std::min( |
| DecayedScore() + BonusIfShortcutLaunched() + BonusIfHasNotifications(), |
| kMaxPoints); |
| } |
| |
| mojom::SiteEngagementDetails SiteEngagementScore::GetDetails() const { |
| mojom::SiteEngagementDetails engagement; |
| engagement.origin = origin_; |
| engagement.base_score = DecayedScore(); |
| engagement.installed_bonus = BonusIfShortcutLaunched(); |
| engagement.notifications_bonus = BonusIfHasNotifications(); |
| engagement.total_score = GetTotalScore(); |
| return engagement; |
| } |
| |
| void SiteEngagementScore::Commit() { |
| DCHECK(settings_map_); |
| if (!UpdateScoreDict(score_dict_.get())) |
| return; |
| |
| settings_map_->SetWebsiteSettingDefaultScope( |
| origin_, GURL(), CONTENT_SETTINGS_TYPE_SITE_ENGAGEMENT, |
| content_settings::ResourceIdentifier(), std::move(score_dict_)); |
| } |
| |
| blink::mojom::EngagementLevel SiteEngagementScore::GetEngagementLevel() const { |
| DCHECK_LT(GetMediumEngagementBoundary(), GetHighEngagementBoundary()); |
| |
| double score = GetTotalScore(); |
| if (score == 0) |
| return blink::mojom::EngagementLevel::NONE; |
| |
| if (score < 1) |
| return blink::mojom::EngagementLevel::MINIMAL; |
| |
| if (score < GetMediumEngagementBoundary()) |
| return blink::mojom::EngagementLevel::LOW; |
| |
| if (score < GetHighEngagementBoundary()) |
| return blink::mojom::EngagementLevel::MEDIUM; |
| |
| if (score < SiteEngagementScore::kMaxPoints) |
| return blink::mojom::EngagementLevel::HIGH; |
| |
| return blink::mojom::EngagementLevel::MAX; |
| } |
| |
| bool SiteEngagementScore::MaxPointsPerDayAdded() const { |
| if (!last_engagement_time_.is_null() && |
| clock_->Now().LocalMidnight() != last_engagement_time_.LocalMidnight()) { |
| return false; |
| } |
| |
| return points_added_today_ == GetMaxPointsPerDay(); |
| } |
| |
| void SiteEngagementScore::Reset(double points, |
| const base::Time last_engagement_time) { |
| raw_score_ = points; |
| points_added_today_ = 0; |
| |
| // This must be set in order to prevent the score from decaying when read. |
| last_engagement_time_ = last_engagement_time; |
| } |
| |
| bool SiteEngagementScore::UpdateScoreDict(base::DictionaryValue* score_dict) { |
| double raw_score_orig = 0; |
| double points_added_today_orig = 0; |
| double last_engagement_time_internal_orig = 0; |
| double last_shortcut_launch_time_internal_orig = 0; |
| |
| score_dict->GetDouble(kRawScoreKey, &raw_score_orig); |
| score_dict->GetDouble(kPointsAddedTodayKey, &points_added_today_orig); |
| score_dict->GetDouble(kLastEngagementTimeKey, |
| &last_engagement_time_internal_orig); |
| score_dict->GetDouble(kLastShortcutLaunchTimeKey, |
| &last_shortcut_launch_time_internal_orig); |
| bool changed = |
| DoublesConsideredDifferent(raw_score_orig, raw_score_, kScoreDelta) || |
| DoublesConsideredDifferent(points_added_today_orig, points_added_today_, |
| kScoreDelta) || |
| DoublesConsideredDifferent(last_engagement_time_internal_orig, |
| last_engagement_time_.ToInternalValue(), |
| kTimeDelta) || |
| DoublesConsideredDifferent(last_shortcut_launch_time_internal_orig, |
| last_shortcut_launch_time_.ToInternalValue(), |
| kTimeDelta); |
| |
| if (!changed) |
| return false; |
| |
| score_dict->SetDouble(kRawScoreKey, raw_score_); |
| score_dict->SetDouble(kPointsAddedTodayKey, points_added_today_); |
| score_dict->SetDouble(kLastEngagementTimeKey, |
| last_engagement_time_.ToInternalValue()); |
| score_dict->SetDouble(kLastShortcutLaunchTimeKey, |
| last_shortcut_launch_time_.ToInternalValue()); |
| |
| return true; |
| } |
| |
| SiteEngagementScore::SiteEngagementScore( |
| base::Clock* clock, |
| const GURL& origin, |
| std::unique_ptr<base::DictionaryValue> score_dict) |
| : clock_(clock), |
| raw_score_(0), |
| points_added_today_(0), |
| last_engagement_time_(), |
| last_shortcut_launch_time_(), |
| score_dict_(score_dict.release()), |
| origin_(origin), |
| settings_map_(nullptr) { |
| if (!score_dict_) |
| return; |
| |
| score_dict_->GetDouble(kRawScoreKey, &raw_score_); |
| score_dict_->GetDouble(kPointsAddedTodayKey, &points_added_today_); |
| |
| double internal_time; |
| if (score_dict_->GetDouble(kLastEngagementTimeKey, &internal_time)) |
| last_engagement_time_ = base::Time::FromInternalValue(internal_time); |
| if (score_dict_->GetDouble(kLastShortcutLaunchTimeKey, &internal_time)) |
| last_shortcut_launch_time_ = base::Time::FromInternalValue(internal_time); |
| } |
| |
| double SiteEngagementScore::DecayedScore() const { |
| // Note that users can change their clock, so from this system's perspective |
| // time can go backwards. If that does happen and the system detects that the |
| // current day is earlier than the last engagement, no decay (or growth) is |
| // applied. |
| int hours_since_engagement = |
| (clock_->Now() - last_engagement_time_).InHours(); |
| if (hours_since_engagement < 0) |
| return raw_score_; |
| |
| int periods = hours_since_engagement / GetDecayPeriodInHours(); |
| return std::max(0.0, raw_score_ * pow(GetDecayProportion(), periods) - |
| periods * GetDecayPoints()); |
| } |
| |
| double SiteEngagementScore::BonusIfShortcutLaunched() const { |
| int days_since_shortcut_launch = |
| (clock_->Now() - last_shortcut_launch_time_).InDays(); |
| if (days_since_shortcut_launch <= kMaxDaysSinceShortcutLaunch) |
| return GetWebAppInstalledPoints(); |
| return 0; |
| } |
| |
| double SiteEngagementScore::BonusIfHasNotifications() const { |
| // TODO(dominickn, raymes): call PermissionManager::GetPermissionStatus when |
| // the PermissionManager is thread-safe. |
| if (settings_map_ && |
| settings_map_->GetContentSetting( |
| origin_, GURL(), CONTENT_SETTINGS_TYPE_NOTIFICATIONS, |
| content_settings::ResourceIdentifier()) == CONTENT_SETTING_ALLOW) { |
| return GetNotificationPermissionPoints(); |
| } |
| |
| return 0; |
| } |