blob: 304a9dd97d721019e5661663b00a89a67c0f38a0 [file] [log] [blame]
// Copyright 2017 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/media/media_engagement_service.h"
#include "base/metrics/histogram_macros.h"
#include "base/time/clock.h"
#include "base/time/default_clock.h"
#include "base/time/time.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/history/history_service_factory.h"
#include "chrome/browser/media/media_engagement_contents_observer.h"
#include "chrome/browser/media/media_engagement_score.h"
#include "chrome/browser/media/media_engagement_service_factory.h"
#include "chrome/browser/prerender/prerender_contents.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/pref_names.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/content_settings.h"
#include "components/history/core/browser/history_service.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/web_contents.h"
#include "media/base/media_switches.h"
const char MediaEngagementService::kHistogramScoreAtStartupName[] =
"Media.Engagement.ScoreAtStartup";
const char MediaEngagementService::kHistogramURLsDeletedScoreReductionName[] =
"Media.Engagement.URLsDeletedScoreReduction";
const char MediaEngagementService::kHistogramClearName[] =
"Media.Engagement.Clear";
namespace {
// The current schema version of the MEI data. If this value is higher
// than the stored value, all MEI data will be wiped.
static const int kSchemaVersion = 4;
// Do not change the values of this enum as it is used for UMA.
enum class MediaEngagementClearReason {
kDataAll = 0,
kDataRange = 1,
kHistoryAll = 2,
kHistoryRange = 3,
kHistoryExpired = 4,
kCount
};
bool MediaEngagementFilterAdapter(
const GURL& predicate,
const ContentSettingsPattern& primary_pattern,
const ContentSettingsPattern& secondary_pattern) {
GURL url(primary_pattern.ToString());
DCHECK(url.is_valid());
return predicate == url;
}
bool MediaEngagementTimeFilterAdapter(
MediaEngagementService* service,
base::Time delete_begin,
base::Time delete_end,
const ContentSettingsPattern& primary_pattern,
const ContentSettingsPattern& secondary_pattern) {
GURL url(primary_pattern.ToString());
DCHECK(url.is_valid());
MediaEngagementScore score = service->CreateEngagementScore(url);
base::Time playback_time = score.last_media_playback_time();
return playback_time >= delete_begin && playback_time <= delete_end;
}
void RecordURLsDeletedScoreReduction(double previous_score,
double current_score) {
int difference = round((previous_score * 100) - (current_score * 100));
DCHECK_GE(difference, 0);
UMA_HISTOGRAM_PERCENTAGE(
MediaEngagementService::kHistogramURLsDeletedScoreReductionName,
difference);
}
void RecordClear(MediaEngagementClearReason reason) {
UMA_HISTOGRAM_ENUMERATION(MediaEngagementService::kHistogramClearName, reason,
MediaEngagementClearReason::kCount);
}
} // namespace
// static
bool MediaEngagementService::IsEnabled() {
return base::FeatureList::IsEnabled(media::kRecordMediaEngagementScores);
}
// static
MediaEngagementService* MediaEngagementService::Get(Profile* profile) {
DCHECK(IsEnabled());
return MediaEngagementServiceFactory::GetForProfile(profile);
}
// static
void MediaEngagementService::CreateWebContentsObserver(
content::WebContents* web_contents) {
DCHECK(IsEnabled());
// Ignore WebContents that are used for prerender/prefetch.
if (prerender::PrerenderContents::FromWebContents(web_contents))
return;
MediaEngagementService* service =
Get(Profile::FromBrowserContext(web_contents->GetBrowserContext()));
if (!service)
return;
service->contents_observers_.insert(
{web_contents,
new MediaEngagementContentsObserver(web_contents, service)});
}
// static
void MediaEngagementService::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterIntegerPref(prefs::kMediaEngagementSchemaVersion, 0, 0);
}
MediaEngagementService::MediaEngagementService(Profile* profile)
: MediaEngagementService(profile, base::DefaultClock::GetInstance()) {}
MediaEngagementService::MediaEngagementService(Profile* profile,
base::Clock* clock)
: profile_(profile), clock_(clock) {
DCHECK(IsEnabled());
// May be null in tests.
history::HistoryService* history = HistoryServiceFactory::GetForProfile(
profile, ServiceAccessType::IMPLICIT_ACCESS);
if (history)
history->AddObserver(this);
// If kSchemaVersion is higher than what we have stored we should wipe
// all Media Engagement data.
if (GetSchemaVersion() < kSchemaVersion) {
HostContentSettingsMapFactory::GetForProfile(profile_)
->ClearSettingsForOneType(CONTENT_SETTINGS_TYPE_MEDIA_ENGAGEMENT);
SetSchemaVersion(kSchemaVersion);
}
// Record the stored scores to a histogram.
task_tracker_.PostTask(
base::ThreadTaskRunnerHandle::Get().get(), FROM_HERE,
base::BindOnce(&MediaEngagementService::RecordStoredScoresToHistogram,
base::Unretained(this)));
}
MediaEngagementService::~MediaEngagementService() {
// Cancel any tasks that depend on |this|.
task_tracker_.TryCancelAll();
}
int MediaEngagementService::GetSchemaVersion() const {
return profile_->GetPrefs()->GetInteger(prefs::kMediaEngagementSchemaVersion);
}
void MediaEngagementService::SetSchemaVersion(int version) {
return profile_->GetPrefs()->SetInteger(prefs::kMediaEngagementSchemaVersion,
version);
}
void MediaEngagementService::ClearDataBetweenTime(
const base::Time& delete_begin,
const base::Time& delete_end) {
if (delete_begin == base::Time() && delete_end == base::Time::Max())
RecordClear(MediaEngagementClearReason::kDataAll);
else
RecordClear(MediaEngagementClearReason::kDataRange);
HostContentSettingsMapFactory::GetForProfile(profile_)
->ClearSettingsForOneTypeWithPredicate(
CONTENT_SETTINGS_TYPE_MEDIA_ENGAGEMENT, base::Time(),
base::Time::Max(),
base::Bind(&MediaEngagementTimeFilterAdapter, this, delete_begin,
delete_end));
}
void MediaEngagementService::Shutdown() {
history::HistoryService* history = HistoryServiceFactory::GetForProfile(
profile_, ServiceAccessType::IMPLICIT_ACCESS);
if (history)
history->RemoveObserver(this);
}
void MediaEngagementService::RecordStoredScoresToHistogram() {
for (const MediaEngagementScore& score : GetAllStoredScores()) {
int percentage = round(score.actual_score() * 100);
UMA_HISTOGRAM_PERCENTAGE(
MediaEngagementService::kHistogramScoreAtStartupName, percentage);
}
}
void MediaEngagementService::OnURLsDeleted(
history::HistoryService* history_service,
const history::DeletionInfo& deletion_info) {
if (deletion_info.IsAllHistory()) {
RecordClear(MediaEngagementClearReason::kHistoryAll);
HostContentSettingsMapFactory::GetForProfile(profile_)
->ClearSettingsForOneType(CONTENT_SETTINGS_TYPE_MEDIA_ENGAGEMENT);
return;
}
// If origins are expired by the history service delete them if they have no
// more visits.
if (deletion_info.is_from_expiration()) {
DCHECK(history_service);
// Build a set of all origins in |deleted_rows|.
std::set<GURL> origins;
for (const history::URLRow& row : deletion_info.deleted_rows()) {
origins.insert(row.url().GetOrigin());
}
// Check if any origins no longer have any visits.
RemoveOriginsWithNoVisits(origins, deletion_info.deleted_urls_origin_map());
return;
}
std::map<GURL, int> origins;
for (const history::URLRow& row : deletion_info.deleted_rows()) {
GURL origin = row.url().GetOrigin();
if (origins.find(origin) == origins.end()) {
origins[origin] = 0;
}
origins[origin]++;
}
if (!origins.empty())
RecordClear(MediaEngagementClearReason::kHistoryRange);
for (auto const& kv : origins) {
// Remove the number of visits consistent with the number
// of URLs from the same origin we are removing.
MediaEngagementScore score = CreateEngagementScore(kv.first);
double original_score = score.actual_score();
score.SetVisits(score.visits() - kv.second);
// If this results in zero visits then clear the score.
if (score.visits() <= 0) {
// Score is now set to 0 so the reduction is equal to the original score.
RecordURLsDeletedScoreReduction(original_score, 0);
Clear(kv.first);
continue;
}
// Otherwise, recalculate the playbacks to keep the
// MEI score consistent.
score.SetMediaPlaybacks(original_score * score.visits());
score.Commit();
RecordURLsDeletedScoreReduction(original_score, score.actual_score());
}
}
void MediaEngagementService::RemoveOriginsWithNoVisits(
const std::set<GURL>& deleted_origins,
const history::OriginCountAndLastVisitMap& origin_data) {
// Find all origins that are in |deleted_origins| and not in
// |remaining_origins| and clear MEI data on them.
bool has_deleted_origins = false;
for (const GURL& origin : deleted_origins) {
const auto& origin_count = origin_data.find(origin);
if (origin_count == origin_data.end() || origin_count->second.first > 0)
continue;
Clear(origin);
has_deleted_origins = true;
}
if (has_deleted_origins)
RecordClear(MediaEngagementClearReason::kHistoryExpired);
}
void MediaEngagementService::Clear(const GURL& url) {
HostContentSettingsMapFactory::GetForProfile(profile_)
->ClearSettingsForOneTypeWithPredicate(
CONTENT_SETTINGS_TYPE_MEDIA_ENGAGEMENT, base::Time(),
base::Time::Max(),
base::Bind(&MediaEngagementFilterAdapter, base::ConstRef(url)));
}
double MediaEngagementService::GetEngagementScore(const GURL& url) const {
return CreateEngagementScore(url).actual_score();
}
bool MediaEngagementService::HasHighEngagement(const GURL& url) const {
return CreateEngagementScore(url).high_score();
}
std::map<GURL, double> MediaEngagementService::GetScoreMapForTesting() const {
std::map<GURL, double> score_map;
for (MediaEngagementScore& score : GetAllStoredScores())
score_map[score.origin()] = score.actual_score();
return score_map;
}
void MediaEngagementService::RecordVisit(const GURL& url) {
if (!ShouldRecordEngagement(url))
return;
MediaEngagementScore score = CreateEngagementScore(url);
score.IncrementVisits();
score.Commit();
}
std::vector<media::mojom::MediaEngagementScoreDetailsPtr>
MediaEngagementService::GetAllScoreDetails() const {
std::vector<MediaEngagementScore> data = GetAllStoredScores();
std::vector<media::mojom::MediaEngagementScoreDetailsPtr> details;
details.reserve(data.size());
for (MediaEngagementScore& score : data)
details.push_back(score.GetScoreDetails());
return details;
}
MediaEngagementScore MediaEngagementService::CreateEngagementScore(
const GURL& url) const {
// If we are in incognito, |settings| will automatically have the data from
// the original profile migrated in, so all engagement scores in incognito
// will be initialised to the values from the original profile.
return MediaEngagementScore(
clock_, url, HostContentSettingsMapFactory::GetForProfile(profile_));
}
MediaEngagementContentsObserver* MediaEngagementService::GetContentsObserverFor(
content::WebContents* web_contents) const {
const auto& it = contents_observers_.find(web_contents);
return it == contents_observers_.end() ? nullptr : it->second;
}
Profile* MediaEngagementService::profile() const {
return profile_;
}
bool MediaEngagementService::ShouldRecordEngagement(const GURL& url) const {
return url.SchemeIsHTTPOrHTTPS();
}
std::vector<MediaEngagementScore> MediaEngagementService::GetAllStoredScores()
const {
ContentSettingsForOneType content_settings;
std::vector<MediaEngagementScore> data;
HostContentSettingsMap* settings =
HostContentSettingsMapFactory::GetForProfile(profile_);
settings->GetSettingsForOneType(CONTENT_SETTINGS_TYPE_MEDIA_ENGAGEMENT,
content_settings::ResourceIdentifier(),
&content_settings);
// `GetSettingsForOneType` mixes incognito and non-incognito results in
// incognito profiles creating duplicates. The incognito results are first so
// we should discard the results following.
std::map<GURL, const ContentSettingPatternSource*> filtered_results;
for (const auto& site : content_settings) {
GURL origin(site.primary_pattern.ToString());
if (!origin.is_valid()) {
NOTREACHED();
continue;
}
const auto& result = filtered_results.find(origin);
if (result != filtered_results.end()) {
DCHECK(result->second->incognito && !site.incognito);
continue;
}
filtered_results[origin] = &site;
}
for (const auto& it : filtered_results) {
const auto& origin = it.first;
auto* const site = it.second;
std::unique_ptr<base::Value> clone =
base::Value::ToUniquePtrValue(site->setting_value.Clone());
data.push_back(MediaEngagementScore(
clock_, origin, base::DictionaryValue::From(std::move(clone)),
settings));
}
return data;
}