1 // Copyright 2017 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/media/media_engagement_score.h"
6
7 #include <utility>
8
9 #include "base/metrics/field_trial_params.h"
10 #include "chrome/browser/engagement/site_engagement_metrics.h"
11 #include "components/content_settings/core/browser/host_content_settings_map.h"
12 #include "components/content_settings/core/common/content_settings.h"
13 #include "components/content_settings/core/common/content_settings_types.h"
14 #include "media/base/media_switches.h"
15
16 const char MediaEngagementScore::kVisitsKey[] = "visits";
17 const char MediaEngagementScore::kMediaPlaybacksKey[] = "mediaPlaybacks";
18 const char MediaEngagementScore::kLastMediaPlaybackTimeKey[] =
19 "lastMediaPlaybackTime";
20 const char MediaEngagementScore::kHasHighScoreKey[] = "hasHighScore";
21
22 const char MediaEngagementScore::kScoreMinVisitsParamName[] = "min_visits";
23 const char MediaEngagementScore::kHighScoreLowerThresholdParamName[] =
24 "lower_threshold";
25 const char MediaEngagementScore::kHighScoreUpperThresholdParamName[] =
26 "upper_threshold";
27
28 namespace {
29
30 const int kScoreMinVisitsParamDefault = 20;
31 const double kHighScoreLowerThresholdParamDefault = 0.2;
32 const double kHighScoreUpperThresholdParamDefault = 0.3;
33
GetMediaEngagementScoreDictForSettings(const HostContentSettingsMap * settings,const url::Origin & origin)34 std::unique_ptr<base::DictionaryValue> GetMediaEngagementScoreDictForSettings(
35 const HostContentSettingsMap* settings,
36 const url::Origin& origin) {
37 if (!settings)
38 return std::make_unique<base::DictionaryValue>();
39
40 std::unique_ptr<base::DictionaryValue> value =
41 base::DictionaryValue::From(settings->GetWebsiteSetting(
42 origin.GetURL(), origin.GetURL(),
43 ContentSettingsType::MEDIA_ENGAGEMENT, nullptr));
44
45 if (value.get())
46 return value;
47 return std::make_unique<base::DictionaryValue>();
48 }
49
GetIntegerFromScore(base::DictionaryValue * dict,base::StringPiece key,int * out)50 void GetIntegerFromScore(base::DictionaryValue* dict,
51 base::StringPiece key,
52 int* out) {
53 if (base::Value* v = dict->FindKeyOfType(key, base::Value::Type::INTEGER))
54 *out = v->GetInt();
55 }
56
57 } // namespace
58
59 // static.
GetHighScoreLowerThreshold()60 double MediaEngagementScore::GetHighScoreLowerThreshold() {
61 return base::GetFieldTrialParamByFeatureAsDouble(
62 media::kMediaEngagementBypassAutoplayPolicies,
63 kHighScoreLowerThresholdParamName, kHighScoreLowerThresholdParamDefault);
64 }
65
66 // static.
GetHighScoreUpperThreshold()67 double MediaEngagementScore::GetHighScoreUpperThreshold() {
68 return base::GetFieldTrialParamByFeatureAsDouble(
69 media::kMediaEngagementBypassAutoplayPolicies,
70 kHighScoreUpperThresholdParamName, kHighScoreUpperThresholdParamDefault);
71 }
72
73 // static.
GetScoreMinVisits()74 int MediaEngagementScore::GetScoreMinVisits() {
75 return base::GetFieldTrialParamByFeatureAsInt(
76 media::kMediaEngagementBypassAutoplayPolicies, kScoreMinVisitsParamName,
77 kScoreMinVisitsParamDefault);
78 }
79
MediaEngagementScore(base::Clock * clock,const url::Origin & origin,HostContentSettingsMap * settings)80 MediaEngagementScore::MediaEngagementScore(base::Clock* clock,
81 const url::Origin& origin,
82 HostContentSettingsMap* settings)
83 : MediaEngagementScore(
84 clock,
85 origin,
86 GetMediaEngagementScoreDictForSettings(settings, origin),
87 settings) {}
88
MediaEngagementScore(base::Clock * clock,const url::Origin & origin,std::unique_ptr<base::DictionaryValue> score_dict,HostContentSettingsMap * settings)89 MediaEngagementScore::MediaEngagementScore(
90 base::Clock* clock,
91 const url::Origin& origin,
92 std::unique_ptr<base::DictionaryValue> score_dict,
93 HostContentSettingsMap* settings)
94 : origin_(origin),
95 clock_(clock),
96 score_dict_(score_dict.release()),
97 settings_map_(settings) {
98 if (!score_dict_)
99 return;
100
101 // This is to prevent using previously saved data to mark an HTTP website as
102 // allowed to autoplay.
103 if (base::FeatureList::IsEnabled(media::kMediaEngagementHTTPSOnly) &&
104 origin_.scheme() != url::kHttpsScheme) {
105 return;
106 }
107
108 GetIntegerFromScore(score_dict_.get(), kVisitsKey, &visits_);
109 GetIntegerFromScore(score_dict_.get(), kMediaPlaybacksKey, &media_playbacks_);
110
111 if (base::Value* value = score_dict_->FindKeyOfType(
112 kHasHighScoreKey, base::Value::Type::BOOLEAN)) {
113 is_high_ = value->GetBool();
114 }
115
116 if (base::Value* value = score_dict_->FindKeyOfType(
117 kLastMediaPlaybackTimeKey, base::Value::Type::DOUBLE)) {
118 last_media_playback_time_ =
119 base::Time::FromInternalValue(value->GetDouble());
120 }
121
122 // Recalculate the total score and high bit. If the high bit changed we
123 // should commit this. This should only happen if we change the threshold
124 // or if we have data from before the bit was introduced.
125 bool was_high = is_high_;
126 Recalculate();
127 bool needs_commit = is_high_ != was_high;
128
129 // If we need to commit because of a migration and we have the settings map
130 // then we should commit.
131 if (needs_commit && settings_map_)
132 Commit();
133 }
134
135 // TODO(beccahughes): Add typemap.
136 media::mojom::MediaEngagementScoreDetailsPtr
GetScoreDetails() const137 MediaEngagementScore::GetScoreDetails() const {
138 return media::mojom::MediaEngagementScoreDetails::New(
139 origin_, actual_score(), visits(), media_playbacks(),
140 last_media_playback_time().ToJsTime(), high_score());
141 }
142
143 MediaEngagementScore::~MediaEngagementScore() = default;
144
145 MediaEngagementScore::MediaEngagementScore(MediaEngagementScore&&) = default;
146 MediaEngagementScore& MediaEngagementScore::operator=(MediaEngagementScore&&) =
147 default;
148
Commit()149 void MediaEngagementScore::Commit() {
150 DCHECK(settings_map_);
151
152 if (origin_.opaque())
153 return;
154
155 if (!UpdateScoreDict())
156 return;
157
158 settings_map_->SetWebsiteSettingDefaultScope(
159 origin_.GetURL(), GURL(), ContentSettingsType::MEDIA_ENGAGEMENT,
160 std::move(score_dict_));
161 }
162
IncrementMediaPlaybacks()163 void MediaEngagementScore::IncrementMediaPlaybacks() {
164 SetMediaPlaybacks(media_playbacks() + 1);
165 last_media_playback_time_ = clock_->Now();
166 }
167
UpdateScoreDict()168 bool MediaEngagementScore::UpdateScoreDict() {
169 int stored_visits = 0;
170 int stored_media_playbacks = 0;
171 double stored_last_media_playback_internal = 0;
172 bool is_high = false;
173
174 if (!score_dict_)
175 return false;
176
177 // This is to prevent saving data that we would otherwise not use.
178 if (base::FeatureList::IsEnabled(media::kMediaEngagementHTTPSOnly) &&
179 origin_.scheme() != url::kHttpsScheme) {
180 return false;
181 }
182
183 if (base::Value* value = score_dict_->FindKeyOfType(
184 kHasHighScoreKey, base::Value::Type::BOOLEAN)) {
185 is_high = value->GetBool();
186 }
187
188 if (base::Value* value = score_dict_->FindKeyOfType(
189 kLastMediaPlaybackTimeKey, base::Value::Type::DOUBLE)) {
190 stored_last_media_playback_internal = value->GetDouble();
191 }
192
193 GetIntegerFromScore(score_dict_.get(), kVisitsKey, &stored_visits);
194 GetIntegerFromScore(score_dict_.get(), kMediaPlaybacksKey,
195 &stored_media_playbacks);
196
197 bool changed = stored_visits != visits() ||
198 stored_media_playbacks != media_playbacks() ||
199 is_high_ != is_high ||
200 stored_last_media_playback_internal !=
201 last_media_playback_time_.ToInternalValue();
202
203 if (!changed)
204 return false;
205
206 score_dict_->SetInteger(kVisitsKey, visits_);
207 score_dict_->SetInteger(kMediaPlaybacksKey, media_playbacks_);
208 score_dict_->SetDouble(kLastMediaPlaybackTimeKey,
209 last_media_playback_time_.ToInternalValue());
210 score_dict_->SetBoolean(kHasHighScoreKey, is_high_);
211
212 // visitsWithMediaTag was deprecated in https://crbug.com/998687 and should
213 // be removed if we see it in |score_dict_|.
214 score_dict_->RemoveKey("visitsWithMediaTag");
215
216 // These keys were deprecated in https://crbug.com/998892 and should be
217 // removed if we see it in |score_dict_|.
218 score_dict_->RemoveKey("audiblePlaybacks");
219 score_dict_->RemoveKey("significantPlaybacks");
220 score_dict_->RemoveKey("highScoreChanges");
221 score_dict_->RemoveKey("mediaElementPlaybacks");
222 score_dict_->RemoveKey("audioContextPlaybacks");
223
224 return true;
225 }
226
Recalculate()227 void MediaEngagementScore::Recalculate() {
228 // Use the minimum visits to compute the score to allow websites that would
229 // surely have a high MEI to pass the bar early.
230 double effective_visits = std::max(visits(), GetScoreMinVisits());
231 actual_score_ = static_cast<double>(media_playbacks()) / effective_visits;
232
233 // Recalculate whether the engagement score is considered high.
234 if (is_high_) {
235 is_high_ = actual_score_ >= GetHighScoreLowerThreshold();
236 } else {
237 is_high_ = actual_score_ >= GetHighScoreUpperThreshold();
238 }
239 }
240
SetVisits(int visits)241 void MediaEngagementScore::SetVisits(int visits) {
242 visits_ = visits;
243 Recalculate();
244 }
245
SetMediaPlaybacks(int media_playbacks)246 void MediaEngagementScore::SetMediaPlaybacks(int media_playbacks) {
247 media_playbacks_ = media_playbacks;
248 Recalculate();
249 }
250