| // 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 <functional> |
| |
| #include "base/bind.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/prefetch/no_state_prefetch/chrome_prerender_contents_delegate.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/no_state_prefetch/browser/prerender_contents.h" |
| #include "components/prefs/pref_service.h" |
| #include "content/public/browser/web_contents.h" |
| #include "media/base/media_switches.h" |
| #include "url/origin.h" |
| |
| 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 url::Origin& predicate, |
| const ContentSettingsPattern& primary_pattern, |
| const ContentSettingsPattern& secondary_pattern) { |
| url::Origin origin = url::Origin::Create(GURL(primary_pattern.ToString())); |
| DCHECK(!origin.opaque()); |
| return predicate == origin; |
| } |
| |
| bool MediaEngagementTimeFilterAdapter( |
| MediaEngagementService* service, |
| base::Time delete_begin, |
| base::Time delete_end, |
| const ContentSettingsPattern& primary_pattern, |
| const ContentSettingsPattern& secondary_pattern) { |
| url::Origin origin = url::Origin::Create(GURL(primary_pattern.ToString())); |
| DCHECK(!origin.opaque()); |
| MediaEngagementScore score = service->CreateEngagementScore(origin); |
| base::Time playback_time = score.last_media_playback_time(); |
| return playback_time >= delete_begin && playback_time <= delete_end; |
| } |
| |
| } // 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::ChromePrerenderContentsDelegate::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(ContentSettingsType::MEDIA_ENGAGEMENT); |
| SetSchemaVersion(kSchemaVersion); |
| } |
| } |
| |
| MediaEngagementService::~MediaEngagementService() = default; |
| |
| 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) { |
| HostContentSettingsMapFactory::GetForProfile(profile_) |
| ->ClearSettingsForOneTypeWithPredicate( |
| ContentSettingsType::MEDIA_ENGAGEMENT, base::Time(), |
| base::Time::Max(), |
| base::BindRepeating(&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::OnURLsDeleted( |
| history::HistoryService* history_service, |
| const history::DeletionInfo& deletion_info) { |
| if (deletion_info.IsAllHistory()) { |
| HostContentSettingsMapFactory::GetForProfile(profile_) |
| ->ClearSettingsForOneType(ContentSettingsType::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<url::Origin> origins; |
| for (const history::URLRow& row : deletion_info.deleted_rows()) { |
| origins.insert(url::Origin::Create(row.url())); |
| } |
| |
| // Check if any origins no longer have any visits. |
| RemoveOriginsWithNoVisits(origins, deletion_info.deleted_urls_origin_map()); |
| return; |
| } |
| |
| std::map<url::Origin, int> origins; |
| for (const history::URLRow& row : deletion_info.deleted_rows()) { |
| url::Origin origin = url::Origin::Create(row.url()); |
| if (origins.find(origin) == origins.end()) { |
| origins[origin] = 0; |
| } |
| origins[origin]++; |
| } |
| |
| 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. |
| Clear(kv.first); |
| continue; |
| } |
| |
| // Otherwise, recalculate the playbacks to keep the |
| // MEI score consistent. |
| score.SetMediaPlaybacks(original_score * score.visits()); |
| score.Commit(); |
| } |
| } |
| |
| void MediaEngagementService::RemoveOriginsWithNoVisits( |
| const std::set<url::Origin>& 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. |
| for (const url::Origin& origin : deleted_origins) { |
| const auto& origin_count = origin_data.find(origin.GetURL()); |
| if (origin_count == origin_data.end() || origin_count->second.first > 0) |
| continue; |
| |
| Clear(origin); |
| } |
| } |
| |
| void MediaEngagementService::Clear(const url::Origin& origin) { |
| HostContentSettingsMapFactory::GetForProfile(profile_) |
| ->ClearSettingsForOneTypeWithPredicate( |
| ContentSettingsType::MEDIA_ENGAGEMENT, base::Time(), |
| base::Time::Max(), |
| base::BindRepeating(&MediaEngagementFilterAdapter, |
| std::cref(origin))); |
| } |
| |
| double MediaEngagementService::GetEngagementScore( |
| const url::Origin& origin) const { |
| return CreateEngagementScore(origin).actual_score(); |
| } |
| |
| bool MediaEngagementService::HasHighEngagement( |
| const url::Origin& origin) const { |
| return CreateEngagementScore(origin).high_score(); |
| } |
| |
| std::map<url::Origin, double> MediaEngagementService::GetScoreMapForTesting() |
| const { |
| std::map<url::Origin, double> score_map; |
| for (MediaEngagementScore& score : GetAllStoredScores()) |
| score_map[score.origin()] = score.actual_score(); |
| return score_map; |
| } |
| |
| void MediaEngagementService::RecordVisit(const url::Origin& origin) { |
| if (!ShouldRecordEngagement(origin)) |
| return; |
| |
| MediaEngagementScore score = CreateEngagementScore(origin); |
| 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 url::Origin& origin) 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_, origin, 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 url::Origin& origin) const { |
| if (base::FeatureList::IsEnabled(media::kMediaEngagementHTTPSOnly)) |
| return origin.scheme() == url::kHttpsScheme; |
| |
| return (origin.scheme() == url::kHttpsScheme || |
| origin.scheme() == url::kHttpScheme); |
| } |
| |
| std::vector<MediaEngagementScore> MediaEngagementService::GetAllStoredScores() |
| const { |
| ContentSettingsForOneType content_settings; |
| std::vector<MediaEngagementScore> data; |
| |
| HostContentSettingsMap* settings = |
| HostContentSettingsMapFactory::GetForProfile(profile_); |
| settings->GetSettingsForOneType(ContentSettingsType::MEDIA_ENGAGEMENT, |
| &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<url::Origin, const ContentSettingPatternSource*> filtered_results; |
| |
| for (const auto& site : content_settings) { |
| url::Origin origin = |
| url::Origin::Create(GURL(site.primary_pattern.ToString())); |
| if (origin.opaque()) { |
| NOTREACHED(); |
| continue; |
| } |
| |
| if (base::FeatureList::IsEnabled(media::kMediaEngagementHTTPSOnly) && |
| origin.scheme() != url::kHttpsScheme) { |
| 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; |
| } |