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