diff options
| author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2021-05-20 09:47:09 +0200 |
|---|---|---|
| committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2021-06-07 11:15:42 +0000 |
| commit | 189d4fd8fad9e3c776873be51938cd31a42b6177 (patch) | |
| tree | 6497caeff5e383937996768766ab3bb2081a40b2 /chromium/components/site_engagement | |
| parent | 8bc75099d364490b22f43a7ce366b366c08f4164 (diff) | |
| download | qtwebengine-chromium-189d4fd8fad9e3c776873be51938cd31a42b6177.tar.gz | |
BASELINE: Update Chromium to 90.0.4430.221
Change-Id: Iff4d9d18d2fcf1a576f3b1f453010f744a232920
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/components/site_engagement')
22 files changed, 2776 insertions, 2 deletions
diff --git a/chromium/components/site_engagement/DIR_METADATA b/chromium/components/site_engagement/DIR_METADATA new file mode 100644 index 00000000000..bd5759d72ad --- /dev/null +++ b/chromium/components/site_engagement/DIR_METADATA @@ -0,0 +1,3 @@ +monorail: { + component: "Internals>Permissions>SiteEngagement" +} diff --git a/chromium/components/site_engagement/OWNERS b/chromium/components/site_engagement/OWNERS index 123948b6d3b..417450b0f71 100644 --- a/chromium/components/site_engagement/OWNERS +++ b/chromium/components/site_engagement/OWNERS @@ -3,4 +3,4 @@ calamity@chromium.org dominickn@chromium.org raymes@chromium.org -# COMPONENT: Internals>Permissions>SiteEngagement +file://chrome/browser/lookalikes/OWNERS diff --git a/chromium/components/site_engagement/content/BUILD.gn b/chromium/components/site_engagement/content/BUILD.gn new file mode 100644 index 00000000000..2f3dcf24003 --- /dev/null +++ b/chromium/components/site_engagement/content/BUILD.gn @@ -0,0 +1,58 @@ +# Copyright 2020 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. + +source_set("content") { + sources = [ + "engagement_type.h", + "site_engagement_metrics.cc", + "site_engagement_metrics.h", + "site_engagement_observer.cc", + "site_engagement_observer.h", + "site_engagement_score.cc", + "site_engagement_score.h", + "site_engagement_service.cc", + "site_engagement_service.h", + ] + + deps = [ + "//base", + "//components/browsing_data/core", + "//components/content_settings/core/browser", + "//components/content_settings/core/common", + "//components/permissions", + "//components/prefs", + "//components/security_state/core", + "//components/site_engagement/core", + "//components/site_engagement/core/mojom:mojo_bindings", + "//components/user_prefs", + "//components/variations", + "//content/public/browser", + "//third_party/blink/public/mojom:mojom_platform_headers", + "//url", + ] + + if (is_android) { + sources += [ + "android/site_engagement_service_android.cc", + "android/site_engagement_service_android.h", + ] + + deps += [ + "//components/embedder_support/android:browser_context", + "//components/site_engagement/content/android:jni_headers", + ] + } +} + +source_set("unit_tests") { + testonly = true + sources = [ "site_engagement_score_unittest.cc" ] + deps = [ + ":content", + "//base", + "//base/test:test_support", + "//components/site_engagement/core/mojom:mojo_bindings", + "//testing/gtest", + ] +} diff --git a/chromium/components/site_engagement/content/DEPS b/chromium/components/site_engagement/content/DEPS new file mode 100644 index 00000000000..4c2117ef692 --- /dev/null +++ b/chromium/components/site_engagement/content/DEPS @@ -0,0 +1,12 @@ +include_rules = [ + "+components/browsing_data", + "+components/content_settings/core", + "+components/keyed_service", + "+components/permissions", + "+components/prefs", + "+components/user_prefs", + "+components/variations", + "+content/public/browser", + "+third_party/blink/public/mojom", + "+ui/base", +] diff --git a/chromium/components/site_engagement/content/android/BUILD.gn b/chromium/components/site_engagement/content/android/BUILD.gn new file mode 100644 index 00000000000..76930b1ffc1 --- /dev/null +++ b/chromium/components/site_engagement/content/android/BUILD.gn @@ -0,0 +1,19 @@ +# Copyright 2020 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. + +import("//build/config/android/rules.gni") + +android_library("java") { + sources = [ "java/src/org/chromium/components/site_engagement/SiteEngagementService.java" ] + deps = [ + "//base:base_java", + "//base:jni_java", + "//components/embedder_support/android:browser_context_java", + ] + annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ] +} + +generate_jni("jni_headers") { + sources = [ "java/src/org/chromium/components/site_engagement/SiteEngagementService.java" ] +} diff --git a/chromium/components/site_engagement/content/android/DEPS b/chromium/components/site_engagement/content/android/DEPS new file mode 100644 index 00000000000..735adf491dd --- /dev/null +++ b/chromium/components/site_engagement/content/android/DEPS @@ -0,0 +1,3 @@ +include_rules = [ + "+components/embedder_support/android", +] diff --git a/chromium/components/site_engagement/content/android/site_engagement_service_android.cc b/chromium/components/site_engagement/content/android/site_engagement_service_android.cc new file mode 100644 index 00000000000..3d954ca40ce --- /dev/null +++ b/chromium/components/site_engagement/content/android/site_engagement_service_android.cc @@ -0,0 +1,85 @@ +// 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 "components/site_engagement/content/android/site_engagement_service_android.h" + +#include "base/android/jni_android.h" +#include "base/android/jni_string.h" +#include "components/embedder_support/android/browser_context/browser_context_handle.h" +#include "components/site_engagement/content/android/jni_headers/SiteEngagementService_jni.h" +#include "components/site_engagement/content/site_engagement_score.h" +#include "components/site_engagement/content/site_engagement_service.h" +#include "url/gurl.h" + +namespace site_engagement { + +using base::android::JavaParamRef; + +// static +const base::android::ScopedJavaGlobalRef<jobject>& +SiteEngagementServiceAndroid::GetOrCreate(JNIEnv* env, + SiteEngagementService* service) { + SiteEngagementServiceAndroid* android_service = service->GetAndroidService(); + if (!android_service) { + service->SetAndroidService( + std::make_unique<SiteEngagementServiceAndroid>(env, service)); + android_service = service->GetAndroidService(); + } + + return android_service->java_service_; +} + +SiteEngagementServiceAndroid::SiteEngagementServiceAndroid( + JNIEnv* env, + SiteEngagementService* service) + : service_(service) { + java_service_.Reset(Java_SiteEngagementService_create( + env, reinterpret_cast<uintptr_t>(this))); +} + +SiteEngagementServiceAndroid::~SiteEngagementServiceAndroid() { + Java_SiteEngagementService_onNativeDestroyed( + base::android::AttachCurrentThread(), java_service_); + java_service_.Reset(); +} + +double SiteEngagementServiceAndroid::GetScore( + JNIEnv* env, + const JavaParamRef<jobject>& caller, + const JavaParamRef<jstring>& jurl) const { + if (!jurl) + return 0; + + return service_->GetScore( + GURL(base::android::ConvertJavaStringToUTF16(env, jurl))); +} + +void SiteEngagementServiceAndroid::ResetBaseScoreForURL( + JNIEnv* env, + const JavaParamRef<jobject>& caller, + const JavaParamRef<jstring>& jurl, + double score) { + if (jurl) { + service_->ResetBaseScoreForURL( + GURL(base::android::ConvertJavaStringToUTF16(env, jurl)), score); + } +} + +void JNI_SiteEngagementService_SetParamValuesForTesting(JNIEnv* env) { + SiteEngagementScore::SetParamValuesForTesting(); +} + +base::android::ScopedJavaLocalRef<jobject> +JNI_SiteEngagementService_SiteEngagementServiceForBrowserContext( + JNIEnv* env, + const base::android::JavaParamRef<jobject>& jhandle) { + SiteEngagementService* service = SiteEngagementService::Get( + browser_context::BrowserContextFromJavaHandle(jhandle)); + DCHECK(service); + + return base::android::ScopedJavaLocalRef<jobject>( + SiteEngagementServiceAndroid::GetOrCreate(env, service)); +} + +} // namespace site_engagement diff --git a/chromium/components/site_engagement/content/android/site_engagement_service_android.h b/chromium/components/site_engagement/content/android/site_engagement_service_android.h new file mode 100644 index 00000000000..653594e6d34 --- /dev/null +++ b/chromium/components/site_engagement/content/android/site_engagement_service_android.h @@ -0,0 +1,52 @@ +// 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. + +#ifndef COMPONENTS_SITE_ENGAGEMENT_CONTENT_ANDROID_SITE_ENGAGEMENT_SERVICE_ANDROID_H_ +#define COMPONENTS_SITE_ENGAGEMENT_CONTENT_ANDROID_SITE_ENGAGEMENT_SERVICE_ANDROID_H_ + +#include "base/android/scoped_java_ref.h" + +namespace site_engagement { + +class SiteEngagementService; + +// Wrapper class to expose the Site Engagement Service to Java. This object is +// owned by the |service_| which it wraps, and is lazily created when a +// Java-side SiteEngagementService is constructed. Once created, all future +// Java-side requests for a SiteEngagementService will use the same native +// object. +// +// This class may only be used on the UI thread. +class SiteEngagementServiceAndroid { + public: + // Returns the Java-side SiteEngagementService object corresponding to + // |service|. + static const base::android::ScopedJavaGlobalRef<jobject>& GetOrCreate( + JNIEnv* env, + SiteEngagementService* service); + + SiteEngagementServiceAndroid(JNIEnv* env, SiteEngagementService* service); + SiteEngagementServiceAndroid(const SiteEngagementServiceAndroid&) = delete; + SiteEngagementServiceAndroid& operator=( + const SiteEngagementServiceAndroid& other) = delete; + + ~SiteEngagementServiceAndroid(); + + double GetScore(JNIEnv* env, + const base::android::JavaParamRef<jobject>& caller, + const base::android::JavaParamRef<jstring>& jurl) const; + + void ResetBaseScoreForURL(JNIEnv* env, + const base::android::JavaParamRef<jobject>& caller, + const base::android::JavaParamRef<jstring>& jurl, + double score); + + private: + base::android::ScopedJavaGlobalRef<jobject> java_service_; + SiteEngagementService* service_; +}; + +} // namespace site_engagement + +#endif // COMPONENTS_SITE_ENGAGEMENT_CONTENT_ANDROID_SITE_ENGAGEMENT_SERVICE_ANDROID_H_ diff --git a/chromium/components/site_engagement/content/engagement_type.h b/chromium/components/site_engagement/content/engagement_type.h new file mode 100644 index 00000000000..71a51a52383 --- /dev/null +++ b/chromium/components/site_engagement/content/engagement_type.h @@ -0,0 +1,31 @@ +// Copyright 2015 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. + +#ifndef COMPONENTS_SITE_ENGAGEMENT_CONTENT_ENGAGEMENT_TYPE_H_ +#define COMPONENTS_SITE_ENGAGEMENT_CONTENT_ENGAGEMENT_TYPE_H_ + +namespace site_engagement { + +// This is used to back a UMA histogram, so it should be treated as +// append-only. Any new values should be inserted immediately prior to +// kLast and added to SiteEngagementServiceEngagementType in +// tools/metrics/histograms/enums.xml. +// TODO(calamity): Document each of these engagement types. +enum class EngagementType { + kNavigation, + kKeypress, + kMouse, + kTouchGesture, + kScroll, + kMediaHidden, + kMediaVisible, + kWebappShortcutLaunch, + kFirstDailyEngagement, + kNotificationInteraction, + kLast, +}; + +} // namespace site_engagement + +#endif // COMPONENTS_SITE_ENGAGEMENT_CONTENT_ENGAGEMENT_TYPE_H_ diff --git a/chromium/components/site_engagement/content/site_engagement_metrics.cc b/chromium/components/site_engagement/content/site_engagement_metrics.cc new file mode 100644 index 00000000000..23c8974971c --- /dev/null +++ b/chromium/components/site_engagement/content/site_engagement_metrics.cc @@ -0,0 +1,135 @@ +// Copyright 2015 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 "components/site_engagement/content/site_engagement_metrics.h" + +#include "base/metrics/histogram_macros.h" +#include "base/stl_util.h" +#include "base/strings/string_number_conversions.h" +#include "components/site_engagement/content/engagement_type.h" +#include "components/site_engagement/content/site_engagement_score.h" + +namespace site_engagement { + +namespace { + +// These numbers are used as suffixes for the +// SiteEngagementService.EngagementScoreBucket_* histogram. If these bases +// change, the EngagementScoreBuckets suffix in histograms.xml should be +// updated. +const int kEngagementBucketHistogramBuckets[] = {0, 10, 20, 30, 40, 50, + 60, 70, 80, 90, 100}; + +} // namespace + +const char SiteEngagementMetrics::kTotalEngagementHistogram[] = + "SiteEngagementService.TotalEngagement"; + +const char SiteEngagementMetrics::kTotalOriginsHistogram[] = + "SiteEngagementService.OriginsEngaged"; + +const char SiteEngagementMetrics::kMeanEngagementHistogram[] = + "SiteEngagementService.MeanEngagement"; + +const char SiteEngagementMetrics::kMedianEngagementHistogram[] = + "SiteEngagementService.MedianEngagement"; + +const char SiteEngagementMetrics::kEngagementScoreHistogram[] = + "SiteEngagementService.EngagementScore"; + +const char SiteEngagementMetrics::kOriginsWithMaxEngagementHistogram[] = + "SiteEngagementService.OriginsWithMaxEngagement"; + +const char SiteEngagementMetrics::kOriginsWithMaxDailyEngagementHistogram[] = + "SiteEngagementService.OriginsWithMaxDailyEngagement"; + +const char SiteEngagementMetrics::kEngagementTypeHistogram[] = + "SiteEngagementService.EngagementType"; + +const char SiteEngagementMetrics::kEngagementBucketHistogramBase[] = + "SiteEngagementService.EngagementScoreBucket_"; + +const char SiteEngagementMetrics::kDaysSinceLastShortcutLaunchHistogram[] = + "SiteEngagementService.DaysSinceLastShortcutLaunch"; + +void SiteEngagementMetrics::RecordTotalSiteEngagement(double total_engagement) { + UMA_HISTOGRAM_COUNTS_10000(kTotalEngagementHistogram, total_engagement); +} + +void SiteEngagementMetrics::RecordTotalOriginsEngaged(int num_origins) { + UMA_HISTOGRAM_COUNTS_10000(kTotalOriginsHistogram, num_origins); +} + +void SiteEngagementMetrics::RecordMeanEngagement(double mean_engagement) { + UMA_HISTOGRAM_COUNTS_100(kMeanEngagementHistogram, mean_engagement); +} + +void SiteEngagementMetrics::RecordMedianEngagement(double median_engagement) { + UMA_HISTOGRAM_COUNTS_100(kMedianEngagementHistogram, median_engagement); +} + +void SiteEngagementMetrics::RecordEngagementScores( + const std::vector<mojom::SiteEngagementDetails>& details) { + if (details.empty()) + return; + + std::map<int, int> score_buckets; + for (size_t i = 0; i < base::size(kEngagementBucketHistogramBuckets); ++i) + score_buckets[kEngagementBucketHistogramBuckets[i]] = 0; + + for (const auto& detail : details) { + double score = detail.total_score; + UMA_HISTOGRAM_COUNTS_100(kEngagementScoreHistogram, score); + + auto bucket = score_buckets.lower_bound(score); + if (bucket == score_buckets.end()) + continue; + + bucket->second++; + } + + for (const auto& b : score_buckets) { + std::string histogram_name = + kEngagementBucketHistogramBase + base::NumberToString(b.first); + + base::LinearHistogram::FactoryGet( + histogram_name, 1, 100, 10, + base::HistogramBase::kUmaTargetedHistogramFlag) + ->Add(b.second * 100 / details.size()); + } +} + +void SiteEngagementMetrics::RecordOriginsWithMaxEngagement(int total_origins) { + UMA_HISTOGRAM_COUNTS_100(kOriginsWithMaxEngagementHistogram, total_origins); +} + +void SiteEngagementMetrics::RecordOriginsWithMaxDailyEngagement( + int total_origins) { + UMA_HISTOGRAM_COUNTS_100(kOriginsWithMaxDailyEngagementHistogram, + total_origins); +} + +void SiteEngagementMetrics::RecordEngagement(EngagementType type) { + UMA_HISTOGRAM_ENUMERATION(kEngagementTypeHistogram, type, + EngagementType::kLast); +} + +void SiteEngagementMetrics::RecordDaysSinceLastShortcutLaunch(int days) { + UMA_HISTOGRAM_COUNTS_100(kDaysSinceLastShortcutLaunchHistogram, days); +} + +// static +std::vector<std::string> +SiteEngagementMetrics::GetEngagementBucketHistogramNames() { + std::vector<std::string> histogram_names; + for (size_t i = 0; i < base::size(kEngagementBucketHistogramBuckets); ++i) { + histogram_names.push_back( + kEngagementBucketHistogramBase + + base::NumberToString(kEngagementBucketHistogramBuckets[i])); + } + + return histogram_names; +} + +} // namespace site_engagement diff --git a/chromium/components/site_engagement/content/site_engagement_metrics.h b/chromium/components/site_engagement/content/site_engagement_metrics.h new file mode 100644 index 00000000000..28c59a61f37 --- /dev/null +++ b/chromium/components/site_engagement/content/site_engagement_metrics.h @@ -0,0 +1,55 @@ +// Copyright 2015 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. + +#ifndef COMPONENTS_SITE_ENGAGEMENT_CONTENT_SITE_ENGAGEMENT_METRICS_H_ +#define COMPONENTS_SITE_ENGAGEMENT_CONTENT_SITE_ENGAGEMENT_METRICS_H_ + +#include <vector> + +#include "base/gtest_prod_util.h" +#include "components/site_engagement/core/mojom/site_engagement_details.mojom.h" +#include "url/gurl.h" + +namespace site_engagement { + +enum class EngagementType; + +// Helper class managing the UMA histograms for the Site Engagement Service. +class SiteEngagementMetrics { + public: + static void RecordTotalSiteEngagement(double total_engagement); + static void RecordTotalOriginsEngaged(int total_origins); + static void RecordMeanEngagement(double mean_engagement); + static void RecordMedianEngagement(double median_engagement); + static void RecordEngagementScores( + const std::vector<mojom::SiteEngagementDetails>& details); + static void RecordOriginsWithMaxEngagement(int total_origins); + static void RecordOriginsWithMaxDailyEngagement(int total_origins); + static void RecordEngagement(EngagementType type); + static void RecordDaysSinceLastShortcutLaunch(int days); + + private: + FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, CheckHistograms); + FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, + GetTotalNotificationPoints); + FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, LastShortcutLaunch); + FRIEND_TEST_ALL_PREFIXES(SiteEngagementHelperTest, + MixedInputEngagementAccumulation); + static const char kTotalEngagementHistogram[]; + static const char kTotalOriginsHistogram[]; + static const char kMeanEngagementHistogram[]; + static const char kMedianEngagementHistogram[]; + static const char kEngagementScoreHistogram[]; + static const char kOriginsWithMaxEngagementHistogram[]; + static const char kOriginsWithMaxDailyEngagementHistogram[]; + static const char kEngagementTypeHistogram[]; + static const char kEngagementBucketHistogramBase[]; + static const char kDaysSinceLastShortcutLaunchHistogram[]; + + static std::vector<std::string> GetEngagementBucketHistogramNames(); +}; + +} // namespace site_engagement + +#endif // COMPONENTS_SITE_ENGAGEMENT_CONTENT_SITE_ENGAGEMENT_METRICS_H_ diff --git a/chromium/components/site_engagement/content/site_engagement_observer.cc b/chromium/components/site_engagement/content/site_engagement_observer.cc new file mode 100644 index 00000000000..2e2a4e6b9a1 --- /dev/null +++ b/chromium/components/site_engagement/content/site_engagement_observer.cc @@ -0,0 +1,40 @@ +// 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 "components/site_engagement/content/site_engagement_observer.h" + +#include "components/site_engagement/content/site_engagement_service.h" + +namespace site_engagement { + +SiteEngagementObserver::SiteEngagementObserver(SiteEngagementService* service) + : service_(nullptr) { + Observe(service); +} + +SiteEngagementObserver::SiteEngagementObserver() : service_(nullptr) {} + +SiteEngagementObserver::~SiteEngagementObserver() { + if (service_) + service_->RemoveObserver(this); +} + +SiteEngagementService* SiteEngagementObserver::GetSiteEngagementService() + const { + return service_; +} + +void SiteEngagementObserver::Observe(SiteEngagementService* service) { + if (service == service_) + return; + + if (service_) + service_->RemoveObserver(this); + + service_ = service; + if (service_) + service->AddObserver(this); +} + +} // namespace site_engagement diff --git a/chromium/components/site_engagement/content/site_engagement_observer.h b/chromium/components/site_engagement/content/site_engagement_observer.h new file mode 100644 index 00000000000..f4a10247469 --- /dev/null +++ b/chromium/components/site_engagement/content/site_engagement_observer.h @@ -0,0 +1,59 @@ +// 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. + +#ifndef COMPONENTS_SITE_ENGAGEMENT_CONTENT_SITE_ENGAGEMENT_OBSERVER_H_ +#define COMPONENTS_SITE_ENGAGEMENT_CONTENT_SITE_ENGAGEMENT_OBSERVER_H_ + +#include "base/gtest_prod_util.h" +#include "base/macros.h" + +namespace content { +class WebContents; +} + +class GURL; + +namespace site_engagement { + +class SiteEngagementService; +enum class EngagementType; + +class SiteEngagementObserver { + public: + // Called when the engagement for |url| loaded in |web_contents| is changed + // to |score|, due to an event of type |type|. This method may be run on user + // input, so observers *must not* perform any expensive tasks here. + // |web_contents| may be null if the engagement has increased when |url| is + // not in a tab, e.g. from a notification interaction. + virtual void OnEngagementEvent(content::WebContents* web_contents, + const GURL& url, + double score, + EngagementType type) {} + + protected: + explicit SiteEngagementObserver(SiteEngagementService* service); + + SiteEngagementObserver(); + + virtual ~SiteEngagementObserver(); + + // Returns the site engagement service which this object is observing. + SiteEngagementService* GetSiteEngagementService() const; + + // Begin observing |service| for engagement increases. + // To stop observing, call Observe(nullptr). + void Observe(SiteEngagementService* service); + + private: + FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, Observers); + friend class SiteEngagementService; + + SiteEngagementService* service_; + + DISALLOW_COPY_AND_ASSIGN(SiteEngagementObserver); +}; + +} // namespace site_engagement + +#endif // COMPONENTS_SITE_ENGAGEMENT_CONTENT_SITE_ENGAGEMENT_OBSERVER_H_ diff --git a/chromium/components/site_engagement/content/site_engagement_score.cc b/chromium/components/site_engagement/content/site_engagement_score.cc new file mode 100644 index 00000000000..fb9c170d22a --- /dev/null +++ b/chromium/components/site_engagement/content/site_engagement_score.cc @@ -0,0 +1,407 @@ +// 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 "components/site_engagement/content/site_engagement_score.h" + +#include <algorithm> +#include <cmath> +#include <utility> + +#include "base/no_destructor.h" +#include "base/strings/string_number_conversions.h" +#include "base/time/clock.h" +#include "base/time/time.h" +#include "base/values.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/site_engagement/content/engagement_type.h" +#include "components/site_engagement/content/site_engagement_metrics.h" +#include "components/variations/variations_associated_data.h" +#include "third_party/blink/public/mojom/site_engagement/site_engagement.mojom.h" + +namespace site_engagement { + +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> GetSiteEngagementScoreDictForSettings( + const HostContentSettingsMap* settings, + const GURL& origin_url) { + if (!settings) + return std::make_unique<base::DictionaryValue>(); + + std::unique_ptr<base::DictionaryValue> value = + base::DictionaryValue::From(settings->GetWebsiteSetting( + origin_url, origin_url, ContentSettingsType::SITE_ENGAGEMENT, NULL)); + + if (value.get()) + return value; + + return std::make_unique<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() { + static base::NoDestructor<ParamValues> param_values([]() { + 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_INTERACTION_POINTS] = { + "notification_interaction_points", 1}; + return param_values; + }()); + 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::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_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, + GetSiteEngagementScoreDictForSettings(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); + + // As the score is about to be updated, commit any decay that has happened + // since the last update. + raw_score_ = DecayedScore(); + + 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( + EngagementType::kFirstDailyEngagement); + } + + 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(), kMaxPoints); +} + +mojom::SiteEngagementDetails SiteEngagementScore::GetDetails() const { + mojom::SiteEngagementDetails engagement; + engagement.origin = origin_; + engagement.base_score = DecayedScore(); + engagement.installed_bonus = BonusIfShortcutLaunched(); + engagement.total_score = GetTotalScore(); + return engagement; +} + +void SiteEngagementScore::Commit() { + DCHECK(settings_map_); + if (!UpdateScoreDict(score_dict_.get())) + return; + + settings_map_->SetWebsiteSettingDefaultScope( + origin_, GURL(), ContentSettingsType::SITE_ENGAGEMENT, + 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; +} + +} // namespace site_engagement diff --git a/chromium/components/site_engagement/content/site_engagement_score.h b/chromium/components/site_engagement/content/site_engagement_score.h new file mode 100644 index 00000000000..2631c85f4a7 --- /dev/null +++ b/chromium/components/site_engagement/content/site_engagement_score.h @@ -0,0 +1,246 @@ +// 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. + +#ifndef COMPONENTS_SITE_ENGAGEMENT_CONTENT_SITE_ENGAGEMENT_SCORE_H_ +#define COMPONENTS_SITE_ENGAGEMENT_CONTENT_SITE_ENGAGEMENT_SCORE_H_ + +#include <array> +#include <memory> +#include <string> +#include <utility> + +#include "base/gtest_prod_util.h" +#include "base/macros.h" +#include "base/time/time.h" +#include "base/values.h" +#include "components/site_engagement/core/mojom/site_engagement_details.mojom-forward.h" +#include "third_party/blink/public/mojom/site_engagement/site_engagement.mojom-forward.h" +#include "url/gurl.h" + +namespace base { +class Clock; +} + +class HostContentSettingsMap; + +namespace site_engagement { + +class SiteEngagementScore { + public: + // The parameters which can be varied via field trial. + enum Variation { + // The maximum number of points that can be accrued in one day. + MAX_POINTS_PER_DAY = 0, + + // The period over which site engagement decays. + DECAY_PERIOD_IN_HOURS, + + // The number of points to decay per period. + DECAY_POINTS, + + // The proportion [0-1] which the current engagement value is multiplied by + // at each decay period, before subtracting DECAY_POINTS. + DECAY_PROPORTION, + + // A score will be erased from the engagement system if it's less than this + // value. + SCORE_CLEANUP_THRESHOLD, + + // The number of points given for navigations. + NAVIGATION_POINTS, + + // The number of points given for user input. + USER_INPUT_POINTS, + + // The number of points given for media playing. Initially calibrated such + // that at least 30 minutes of foreground media would be required to allow a + // site to reach the daily engagement maximum. + VISIBLE_MEDIA_POINTS, + HIDDEN_MEDIA_POINTS, + + // The number of points added to engagement when a site is launched from + // homescreen or added as a bookmark app. This bonus will apply for ten days + // following a launch; each new launch resets the ten days. + WEB_APP_INSTALLED_POINTS, + + // The number of points given for the first engagement event of the day for + // each site. + FIRST_DAILY_ENGAGEMENT, + + // The number of points that the engagement service must accumulate to be + // considered 'useful'. + BOOTSTRAP_POINTS, + + // The boundaries between low/medium and medium/high engagement as returned + // by GetEngagementLevel(). + MEDIUM_ENGAGEMENT_BOUNDARY, + HIGH_ENGAGEMENT_BOUNDARY, + + // The maximum number of decays that a SiteEngagementScore can incur before + // entering a grace period. MAX_DECAYS_PER_SCORE * DECAY_PERIOD_IN_DAYS is + // the max decay period, i.e. the maximum duration permitted for + // (clock_->Now() - score.last_engagement_time()). + MAX_DECAYS_PER_SCORE, + + // If a SiteEngagamentScore has not been accessed or updated for a period + // longer than the max decay period + LAST_ENGAGEMENT_GRACE_PERIOD_IN_HOURS + // (see above), its last engagement time will be reset to be max decay + // period prior to clock_->Now(). + LAST_ENGAGEMENT_GRACE_PERIOD_IN_HOURS, + + // The number of points given for interacting with a displayed notification. + NOTIFICATION_INTERACTION_POINTS, + + MAX_VARIATION + }; + + // The maximum number of points that are allowed. + static const double kMaxPoints; + + static double GetMaxPointsPerDay(); + static double GetDecayPeriodInHours(); + static double GetDecayPoints(); + static double GetDecayProportion(); + static double GetScoreCleanupThreshold(); + static double GetNavigationPoints(); + static double GetUserInputPoints(); + static double GetVisibleMediaPoints(); + static double GetHiddenMediaPoints(); + static double GetWebAppInstalledPoints(); + static double GetFirstDailyEngagementPoints(); + static double GetBootstrapPoints(); + static double GetMediumEngagementBoundary(); + static double GetHighEngagementBoundary(); + static double GetMaxDecaysPerScore(); + static double GetLastEngagementGracePeriodInHours(); + static double GetNotificationInteractionPoints(); + + // Sets fixed parameter values for testing site engagement. Ensure that any + // newly added parameters receive a fixed value here. + static void SetParamValuesForTesting(); + + // Update the default engagement settings via variations. + static void UpdateFromVariations(const char* param_name); + + // The SiteEngagementScore does not take ownership of |clock|. It is the + // responsibility of the caller to make sure |clock| outlives this + // SiteEngagementScore. + SiteEngagementScore(base::Clock* clock, + const GURL& origin, + HostContentSettingsMap* settings); + SiteEngagementScore(SiteEngagementScore&& other); + ~SiteEngagementScore(); + + SiteEngagementScore& operator=(SiteEngagementScore&& other); + + // Adds |points| to this score, respecting daily limits and the maximum + // possible score. Decays the score if it has not been updated recently + // enough. + void AddPoints(double points); + + // Returns the total score, taking into account the base, bonus and maximum + // values. + double GetTotalScore() const; + + // Returns a structure containing the origin URL and score, and details + // of the base and bonus scores. Note that the |score| is limited to + // kMaxPoints, while the detailed scores are returned raw. + mojom::SiteEngagementDetails GetDetails() const; + + // Writes the values in this score into |settings_map_|. + void Commit(); + + // Returns the discrete engagement level for this score. + blink::mojom::EngagementLevel GetEngagementLevel() const; + + // Returns true if the maximum number of points today has been added. + bool MaxPointsPerDayAdded() const; + + // Resets the score to |points| and resets the daily point limit. If + // |updated_time| is non-null, sets the last engagement time to that value. + void Reset(double points, const base::Time updated_time); + + // Get/set the last time this origin was launched from an installed shortcut. + base::Time last_shortcut_launch_time() const { + return last_shortcut_launch_time_; + } + void set_last_shortcut_launch_time(const base::Time& time) { + last_shortcut_launch_time_ = time; + } + + // Get/set the last time this origin recorded an engagement change. + base::Time last_engagement_time() const { return last_engagement_time_; } + void set_last_engagement_time(const base::Time& time) { + last_engagement_time_ = time; + } + + private: + FRIEND_TEST_ALL_PREFIXES(SiteEngagementScoreTest, FirstDailyEngagementBonus); + FRIEND_TEST_ALL_PREFIXES(SiteEngagementScoreTest, PartiallyEmptyDictionary); + FRIEND_TEST_ALL_PREFIXES(SiteEngagementScoreTest, PopulatedDictionary); + FRIEND_TEST_ALL_PREFIXES(SiteEngagementScoreTest, Reset); + friend class SiteEngagementScoreTest; + friend class SiteEngagementServiceTest; + + using ParamValues = std::array<std::pair<std::string, double>, MAX_VARIATION>; + + // Array holding the values corresponding to each item in Variation array. + static ParamValues& GetParamValues(); + + // Keys used in the content settings dictionary. + static const char kRawScoreKey[]; + static const char kPointsAddedTodayKey[]; + static const char kLastEngagementTimeKey[]; + static const char kLastShortcutLaunchTimeKey[]; + + // This version of the constructor is used in unit tests. + SiteEngagementScore(base::Clock* clock, + const GURL& origin, + std::unique_ptr<base::DictionaryValue> score_dict); + + // Determine the score, accounting for any decay. + double DecayedScore() const; + + // Determine bonus from being installed, and having been launched recently.. + double BonusIfShortcutLaunched() const; + + // Updates the content settings dictionary |score_dict| with the current score + // fields. Returns true if |score_dict| changed, otherwise return false. + bool UpdateScoreDict(base::DictionaryValue* score_dict); + + // The clock used to vend times. Enables time travelling in tests. Owned by + // the SiteEngagementService. + base::Clock* clock_; + + // |raw_score_| is the score before any decay is applied. + double raw_score_; + + // The points added 'today' are tracked to avoid adding more than + // kMaxPointsPerDay on any one day. 'Today' is defined in local time. + double points_added_today_; + + // The last time the score was updated for engagement. Used in conjunction + // with |points_added_today_| to avoid adding more than kMaxPointsPerDay on + // any one day. + base::Time last_engagement_time_; + + // The last time the site with this score was launched from an installed + // shortcut. + base::Time last_shortcut_launch_time_; + + // The dictionary that represents this engagement score. + std::unique_ptr<base::DictionaryValue> score_dict_; + + // The origin this score represents. + GURL origin_; + + // The settings to write this score to when Commit() is called. + HostContentSettingsMap* settings_map_; + + DISALLOW_COPY_AND_ASSIGN(SiteEngagementScore); +}; + +} // namespace site_engagement + +#endif // COMPONENTS_SITE_ENGAGEMENT_CONTENT_SITE_ENGAGEMENT_SCORE_H_ diff --git a/chromium/components/site_engagement/content/site_engagement_score_unittest.cc b/chromium/components/site_engagement/content/site_engagement_score_unittest.cc new file mode 100644 index 00000000000..09cae4d3406 --- /dev/null +++ b/chromium/components/site_engagement/content/site_engagement_score_unittest.cc @@ -0,0 +1,480 @@ +// 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 "components/site_engagement/content/site_engagement_score.h" + +#include <utility> + +#include "base/macros.h" +#include "base/test/simple_test_clock.h" +#include "base/values.h" +#include "components/site_engagement/core/mojom/site_engagement_details.mojom.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace site_engagement { + +namespace { + +const int kLessAccumulationsThanNeededToMaxDailyEngagement = 2; +const int kMoreAccumulationsThanNeededToMaxDailyEngagement = 40; +const int kMoreAccumulationsThanNeededToMaxTotalEngagement = 200; +const int kLessDaysThanNeededToMaxTotalEngagement = 4; +const int kMoreDaysThanNeededToMaxTotalEngagement = 40; +const int kLessPeriodsThanNeededToDecayMaxScore = 2; +const int kMorePeriodsThanNeededToDecayMaxScore = 40; +const double kMaxRoundingDeviation = 0.0001; + +base::Time GetReferenceTime() { + base::Time::Exploded exploded_reference_time; + exploded_reference_time.year = 2015; + exploded_reference_time.month = 1; + exploded_reference_time.day_of_month = 30; + exploded_reference_time.day_of_week = 5; + exploded_reference_time.hour = 11; + exploded_reference_time.minute = 0; + exploded_reference_time.second = 0; + exploded_reference_time.millisecond = 0; + + base::Time out_time; + EXPECT_TRUE( + base::Time::FromLocalExploded(exploded_reference_time, &out_time)); + return out_time; +} + +} // namespace + +class SiteEngagementScoreTest : public testing::Test { + public: + SiteEngagementScoreTest() : score_(&test_clock_, GURL(), nullptr) {} + + void SetUp() override { + // Disable the first engagement bonus for tests. + SiteEngagementScore::SetParamValuesForTesting(); + } + + protected: + void VerifyScore(const SiteEngagementScore& score, + double expected_raw_score, + double expected_points_added_today, + base::Time expected_last_engagement_time) { + EXPECT_EQ(expected_raw_score, score.raw_score_); + EXPECT_EQ(expected_points_added_today, score.points_added_today_); + EXPECT_EQ(expected_last_engagement_time, score.last_engagement_time_); + } + + void UpdateScore(SiteEngagementScore* score, + double raw_score, + double points_added_today, + base::Time last_engagement_time) { + score->raw_score_ = raw_score; + score->points_added_today_ = points_added_today; + score->last_engagement_time_ = last_engagement_time; + } + + void TestScoreInitializesAndUpdates( + std::unique_ptr<base::DictionaryValue> score_dict, + double expected_raw_score, + double expected_points_added_today, + base::Time expected_last_engagement_time) { + std::unique_ptr<base::DictionaryValue> copy(score_dict->DeepCopy()); + SiteEngagementScore initial_score(&test_clock_, GURL(), + std::move(score_dict)); + VerifyScore(initial_score, expected_raw_score, expected_points_added_today, + expected_last_engagement_time); + + // Updating the score dict should return false, as the score shouldn't + // have changed at this point. + EXPECT_FALSE(initial_score.UpdateScoreDict(copy.get())); + + // Update the score to new values and verify it updates the score dict + // correctly. + base::Time different_day = + GetReferenceTime() + base::TimeDelta::FromDays(1); + UpdateScore(&initial_score, 5, 10, different_day); + EXPECT_TRUE(initial_score.UpdateScoreDict(copy.get())); + SiteEngagementScore updated_score(&test_clock_, GURL(), std::move(copy)); + VerifyScore(updated_score, 5, 10, different_day); + } + + void SetParamValue(SiteEngagementScore::Variation variation, double value) { + SiteEngagementScore::GetParamValues()[variation].second = value; + } + + base::SimpleTestClock test_clock_; + SiteEngagementScore score_; +}; + +// Accumulate score many times on the same day. Ensure each time the score goes +// up, but not more than the maximum per day. +TEST_F(SiteEngagementScoreTest, AccumulateOnSameDay) { + base::Time reference_time = GetReferenceTime(); + + test_clock_.SetNow(reference_time); + for (int i = 0; i < kMoreAccumulationsThanNeededToMaxDailyEngagement; ++i) { + score_.AddPoints(SiteEngagementScore::GetNavigationPoints()); + EXPECT_EQ(std::min(SiteEngagementScore::GetMaxPointsPerDay(), + (i + 1) * SiteEngagementScore::GetNavigationPoints()), + score_.GetTotalScore()); + } + + EXPECT_EQ(SiteEngagementScore::GetMaxPointsPerDay(), score_.GetTotalScore()); +} + +// Accumulate on the first day to max that day's engagement, then accumulate on +// a different day. +TEST_F(SiteEngagementScoreTest, AccumulateOnTwoDays) { + base::Time reference_time = GetReferenceTime(); + base::Time later_date = reference_time + base::TimeDelta::FromDays(2); + + test_clock_.SetNow(reference_time); + for (int i = 0; i < kMoreAccumulationsThanNeededToMaxDailyEngagement; ++i) + score_.AddPoints(SiteEngagementScore::GetNavigationPoints()); + + EXPECT_EQ(SiteEngagementScore::GetMaxPointsPerDay(), score_.GetTotalScore()); + + test_clock_.SetNow(later_date); + for (int i = 0; i < kMoreAccumulationsThanNeededToMaxDailyEngagement; ++i) { + score_.AddPoints(SiteEngagementScore::GetNavigationPoints()); + double day_score = + std::min(SiteEngagementScore::GetMaxPointsPerDay(), + (i + 1) * SiteEngagementScore::GetNavigationPoints()); + EXPECT_EQ(day_score + SiteEngagementScore::GetMaxPointsPerDay(), + score_.GetTotalScore()); + } + + EXPECT_EQ(2 * SiteEngagementScore::GetMaxPointsPerDay(), + score_.GetTotalScore()); +} + +// Accumulate score on many consecutive days and ensure the score doesn't exceed +// the maximum allowed. +TEST_F(SiteEngagementScoreTest, AccumulateALotOnManyDays) { + base::Time current_day = GetReferenceTime(); + + for (int i = 0; i < kMoreDaysThanNeededToMaxTotalEngagement; ++i) { + current_day += base::TimeDelta::FromDays(1); + test_clock_.SetNow(current_day); + for (int j = 0; j < kMoreAccumulationsThanNeededToMaxDailyEngagement; ++j) + score_.AddPoints(SiteEngagementScore::GetNavigationPoints()); + + EXPECT_EQ(std::min(SiteEngagementScore::kMaxPoints, + (i + 1) * SiteEngagementScore::GetMaxPointsPerDay()), + score_.GetTotalScore()); + } + + EXPECT_EQ(SiteEngagementScore::kMaxPoints, score_.GetTotalScore()); +} + +// Accumulate a little on many consecutive days and ensure the score doesn't +// exceed the maximum allowed. +TEST_F(SiteEngagementScoreTest, AccumulateALittleOnManyDays) { + base::Time current_day = GetReferenceTime(); + + for (int i = 0; i < kMoreAccumulationsThanNeededToMaxTotalEngagement; ++i) { + current_day += base::TimeDelta::FromDays(1); + test_clock_.SetNow(current_day); + + for (int j = 0; j < kLessAccumulationsThanNeededToMaxDailyEngagement; ++j) + score_.AddPoints(SiteEngagementScore::GetNavigationPoints()); + + EXPECT_EQ( + std::min(SiteEngagementScore::kMaxPoints, + (i + 1) * kLessAccumulationsThanNeededToMaxDailyEngagement * + SiteEngagementScore::GetNavigationPoints()), + score_.GetTotalScore()); + } + + EXPECT_EQ(SiteEngagementScore::kMaxPoints, score_.GetTotalScore()); +} + +// Accumulate a bit, then check the score decays properly for a range of times. +TEST_F(SiteEngagementScoreTest, ScoresDecayOverTime) { + base::Time current_day = GetReferenceTime(); + + // First max the score. + for (int i = 0; i < kMoreDaysThanNeededToMaxTotalEngagement; ++i) { + current_day += base::TimeDelta::FromDays(1); + test_clock_.SetNow(current_day); + + for (int j = 0; j < kMoreAccumulationsThanNeededToMaxDailyEngagement; ++j) + score_.AddPoints(SiteEngagementScore::GetNavigationPoints()); + } + + EXPECT_EQ(SiteEngagementScore::kMaxPoints, score_.GetTotalScore()); + + // The score should not have decayed before the first decay period has + // elapsed. + test_clock_.SetNow(current_day + + base::TimeDelta::FromHours( + SiteEngagementScore::GetDecayPeriodInHours() - 1)); + EXPECT_EQ(SiteEngagementScore::kMaxPoints, score_.GetTotalScore()); + + // The score should have decayed by one chunk after one decay period has + // elapsed. + test_clock_.SetNow( + current_day + + base::TimeDelta::FromHours(SiteEngagementScore::GetDecayPeriodInHours())); + EXPECT_EQ( + SiteEngagementScore::kMaxPoints - SiteEngagementScore::GetDecayPoints(), + score_.GetTotalScore()); + + // The score should have decayed by the right number of chunks after a few + // decay periods have elapsed. + test_clock_.SetNow( + current_day + + base::TimeDelta::FromHours(kLessPeriodsThanNeededToDecayMaxScore * + SiteEngagementScore::GetDecayPeriodInHours())); + EXPECT_EQ(SiteEngagementScore::kMaxPoints - + kLessPeriodsThanNeededToDecayMaxScore * + SiteEngagementScore::GetDecayPoints(), + score_.GetTotalScore()); + + // The score should not decay below zero. + test_clock_.SetNow( + current_day + + base::TimeDelta::FromHours(kMorePeriodsThanNeededToDecayMaxScore * + SiteEngagementScore::GetDecayPeriodInHours())); + EXPECT_EQ(0, score_.GetTotalScore()); +} + +// Test that any expected decays are applied before adding points. +TEST_F(SiteEngagementScoreTest, DecaysAppliedBeforeAdd) { + base::Time current_day = GetReferenceTime(); + + // Get the score up to something that can handle a bit of decay before + for (int i = 0; i < kLessDaysThanNeededToMaxTotalEngagement; ++i) { + current_day += base::TimeDelta::FromDays(1); + test_clock_.SetNow(current_day); + + for (int j = 0; j < kMoreAccumulationsThanNeededToMaxDailyEngagement; ++j) + score_.AddPoints(SiteEngagementScore::GetNavigationPoints()); + } + + double initial_score = kLessDaysThanNeededToMaxTotalEngagement * + SiteEngagementScore::GetMaxPointsPerDay(); + EXPECT_EQ(initial_score, score_.GetTotalScore()); + + // Go forward a few decay periods. + test_clock_.SetNow( + current_day + + base::TimeDelta::FromHours(kLessPeriodsThanNeededToDecayMaxScore * + SiteEngagementScore::GetDecayPeriodInHours())); + + double decayed_score = + initial_score - kLessPeriodsThanNeededToDecayMaxScore * + SiteEngagementScore::GetDecayPoints(); + EXPECT_EQ(decayed_score, score_.GetTotalScore()); + + // Now add some points. + score_.AddPoints(SiteEngagementScore::GetNavigationPoints()); + EXPECT_EQ(decayed_score + SiteEngagementScore::GetNavigationPoints(), + score_.GetTotalScore()); +} + +// Test that going back in time is handled properly. +TEST_F(SiteEngagementScoreTest, GoBackInTime) { + base::Time current_day = GetReferenceTime(); + + test_clock_.SetNow(current_day); + for (int i = 0; i < kMoreAccumulationsThanNeededToMaxDailyEngagement; ++i) + score_.AddPoints(SiteEngagementScore::GetNavigationPoints()); + + EXPECT_EQ(SiteEngagementScore::GetMaxPointsPerDay(), score_.GetTotalScore()); + + // Adding to the score on an earlier date should be treated like another day, + // and should not cause any decay. + test_clock_.SetNow(current_day - base::TimeDelta::FromDays( + kMorePeriodsThanNeededToDecayMaxScore * + SiteEngagementScore::GetDecayPoints())); + for (int i = 0; i < kMoreAccumulationsThanNeededToMaxDailyEngagement; ++i) { + score_.AddPoints(SiteEngagementScore::GetNavigationPoints()); + double day_score = + std::min(SiteEngagementScore::GetMaxPointsPerDay(), + (i + 1) * SiteEngagementScore::GetNavigationPoints()); + EXPECT_EQ(day_score + SiteEngagementScore::GetMaxPointsPerDay(), + score_.GetTotalScore()); + } + + EXPECT_EQ(2 * SiteEngagementScore::GetMaxPointsPerDay(), + score_.GetTotalScore()); +} + +// Test that scores are read / written correctly from / to empty score +// dictionaries. +TEST_F(SiteEngagementScoreTest, EmptyDictionary) { + std::unique_ptr<base::DictionaryValue> dict(new base::DictionaryValue()); + TestScoreInitializesAndUpdates(std::move(dict), 0, 0, base::Time()); +} + +// Test that scores are read / written correctly from / to partially empty +// score dictionaries. +TEST_F(SiteEngagementScoreTest, PartiallyEmptyDictionary) { + std::unique_ptr<base::DictionaryValue> dict(new base::DictionaryValue()); + dict->SetDouble(SiteEngagementScore::kPointsAddedTodayKey, 2); + + TestScoreInitializesAndUpdates(std::move(dict), 0, 2, base::Time()); +} + +// Test that scores are read / written correctly from / to populated score +// dictionaries. +TEST_F(SiteEngagementScoreTest, PopulatedDictionary) { + std::unique_ptr<base::DictionaryValue> dict(new base::DictionaryValue()); + dict->SetDouble(SiteEngagementScore::kRawScoreKey, 1); + dict->SetDouble(SiteEngagementScore::kPointsAddedTodayKey, 2); + dict->SetDouble(SiteEngagementScore::kLastEngagementTimeKey, + GetReferenceTime().ToInternalValue()); + + TestScoreInitializesAndUpdates(std::move(dict), 1, 2, GetReferenceTime()); +} + +// Ensure bonus engagement is awarded for the first engagement of a day. +TEST_F(SiteEngagementScoreTest, FirstDailyEngagementBonus) { + SetParamValue(SiteEngagementScore::FIRST_DAILY_ENGAGEMENT, 0.5); + + SiteEngagementScore score1(&test_clock_, GURL(), + std::unique_ptr<base::DictionaryValue>()); + SiteEngagementScore score2(&test_clock_, GURL(), + std::unique_ptr<base::DictionaryValue>()); + base::Time current_day = GetReferenceTime(); + + test_clock_.SetNow(current_day); + + // The first engagement event gets the bonus. + score1.AddPoints(0.5); + EXPECT_EQ(1.0, score1.GetTotalScore()); + + // Subsequent events do not. + score1.AddPoints(0.5); + EXPECT_EQ(1.5, score1.GetTotalScore()); + + // Bonuses are awarded independently between scores. + score2.AddPoints(1.0); + EXPECT_EQ(1.5, score2.GetTotalScore()); + score2.AddPoints(1.0); + EXPECT_EQ(2.5, score2.GetTotalScore()); + + test_clock_.SetNow(current_day + base::TimeDelta::FromDays(1)); + + // The first event for the next day gets the bonus. + score1.AddPoints(0.5); + EXPECT_EQ(2.5, score1.GetTotalScore()); + + // Subsequent events do not. + score1.AddPoints(0.5); + EXPECT_EQ(3.0, score1.GetTotalScore()); + + score2.AddPoints(1.0); + EXPECT_EQ(4.0, score2.GetTotalScore()); + score2.AddPoints(1.0); + EXPECT_EQ(5.0, score2.GetTotalScore()); +} + +// Test that resetting a score has the correct properties. +TEST_F(SiteEngagementScoreTest, Reset) { + base::Time current_day = GetReferenceTime(); + + test_clock_.SetNow(current_day); + score_.AddPoints(SiteEngagementScore::GetNavigationPoints()); + EXPECT_EQ(SiteEngagementScore::GetNavigationPoints(), score_.GetTotalScore()); + + current_day += base::TimeDelta::FromDays(7); + test_clock_.SetNow(current_day); + + score_.Reset(20.0, current_day); + EXPECT_DOUBLE_EQ(20.0, score_.GetTotalScore()); + EXPECT_DOUBLE_EQ(0, score_.points_added_today_); + EXPECT_EQ(current_day, score_.last_engagement_time_); + EXPECT_TRUE(score_.last_shortcut_launch_time_.is_null()); + + // Adding points after the reset should work as normal. + score_.AddPoints(5); + EXPECT_EQ(25.0, score_.GetTotalScore()); + + // The decay should happen one decay period from the current time. + test_clock_.SetNow(current_day + + base::TimeDelta::FromHours( + SiteEngagementScore::GetDecayPeriodInHours() + 1)); + EXPECT_EQ(25.0 - SiteEngagementScore::GetDecayPoints(), + score_.GetTotalScore()); + + // Ensure that manually setting a time works as expected. + score_.AddPoints(5); + test_clock_.SetNow(GetReferenceTime()); + base::Time now = test_clock_.Now(); + score_.Reset(10.0, now); + + EXPECT_DOUBLE_EQ(10.0, score_.GetTotalScore()); + EXPECT_DOUBLE_EQ(0, score_.points_added_today_); + EXPECT_EQ(now, score_.last_engagement_time_); + EXPECT_TRUE(score_.last_shortcut_launch_time_.is_null()); + + base::Time old_now = test_clock_.Now(); + + score_.set_last_shortcut_launch_time(test_clock_.Now()); + test_clock_.SetNow(GetReferenceTime() + base::TimeDelta::FromDays(3)); + now = test_clock_.Now(); + score_.Reset(15.0, now); + + // 5 bonus from the last shortcut launch. + EXPECT_DOUBLE_EQ(20.0, score_.GetTotalScore()); + EXPECT_DOUBLE_EQ(0, score_.points_added_today_); + EXPECT_EQ(now, score_.last_engagement_time_); + EXPECT_EQ(old_now, score_.last_shortcut_launch_time_); +} + +// Test proportional decay. +TEST_F(SiteEngagementScoreTest, ProportionalDecay) { + SetParamValue(SiteEngagementScore::DECAY_PROPORTION, 0.5); + SetParamValue(SiteEngagementScore::DECAY_POINTS, 0); + SetParamValue(SiteEngagementScore::MAX_POINTS_PER_DAY, 20); + base::Time current_day = GetReferenceTime(); + test_clock_.SetNow(current_day); + + // Single decay period, expect the score to be halved once. + score_.AddPoints(2.0); + current_day += base::TimeDelta::FromDays(7); + test_clock_.SetNow(current_day); + EXPECT_DOUBLE_EQ(1.0, score_.GetTotalScore()); + + // 3 decay periods, expect the score to be halved 3 times. + score_.AddPoints(15.0); + current_day += base::TimeDelta::FromDays(21); + test_clock_.SetNow(current_day); + EXPECT_DOUBLE_EQ(2.0, score_.GetTotalScore()); + + // Ensure point removal happens after proportional decay. + score_.AddPoints(4.0); + EXPECT_DOUBLE_EQ(6.0, score_.GetTotalScore()); + SetParamValue(SiteEngagementScore::DECAY_POINTS, 2.0); + current_day += base::TimeDelta::FromDays(7); + test_clock_.SetNow(current_day); + EXPECT_NEAR(1.0, score_.GetTotalScore(), kMaxRoundingDeviation); +} + +// Verify that GetDetails fills out all fields correctly. +TEST_F(SiteEngagementScoreTest, GetDetails) { + // Advance the clock, otherwise Now() is the same as the null Time value. + test_clock_.Advance(base::TimeDelta::FromDays(365)); + + GURL url("http://www.google.com/"); + + // Replace |score_| with one with an actual URL. + score_ = SiteEngagementScore(&test_clock_, url, nullptr); + + // Initially all component scores should be zero. + mojom::SiteEngagementDetails details = score_.GetDetails(); + EXPECT_DOUBLE_EQ(0.0, details.total_score); + EXPECT_DOUBLE_EQ(0.0, details.installed_bonus); + EXPECT_DOUBLE_EQ(0.0, details.base_score); + EXPECT_EQ(url, details.origin); + + // Simulate the app having been launched. + score_.set_last_shortcut_launch_time(test_clock_.Now()); + details = score_.GetDetails(); + EXPECT_DOUBLE_EQ(details.installed_bonus, details.total_score); + EXPECT_LT(0.0, details.installed_bonus); + EXPECT_DOUBLE_EQ(0.0, details.base_score); +} + +} // namespace site_engagement diff --git a/chromium/components/site_engagement/content/site_engagement_service.cc b/chromium/components/site_engagement/content/site_engagement_service.cc new file mode 100644 index 00000000000..7dadb81e1f6 --- /dev/null +++ b/chromium/components/site_engagement/content/site_engagement_service.cc @@ -0,0 +1,705 @@ +// Copyright 2015 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 "components/site_engagement/content/site_engagement_service.h" + +#include <stddef.h> + +#include <algorithm> +#include <utility> + +#include "base/bind.h" +#include "base/memory/scoped_refptr.h" +#include "base/metrics/field_trial.h" +#include "base/strings/string_util.h" +#include "base/task/thread_pool.h" +#include "base/time/clock.h" +#include "base/time/default_clock.h" +#include "base/time/time.h" +#include "base/trace_event/trace_event.h" +#include "base/values.h" +#include "components/browsing_data/core/browsing_data_utils.h" +#include "components/content_settings/core/browser/host_content_settings_map.h" +#include "components/content_settings/core/common/content_settings_pattern.h" +#include "components/permissions/permissions_client.h" +#include "components/prefs/pref_registry_simple.h" +#include "components/prefs/pref_service.h" +#include "components/site_engagement/content/engagement_type.h" +#include "components/site_engagement/content/site_engagement_metrics.h" +#include "components/site_engagement/content/site_engagement_observer.h" +#include "components/site_engagement/content/site_engagement_score.h" +#include "components/site_engagement/core/pref_names.h" +#include "components/user_prefs/user_prefs.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/browser_task_traits.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/web_contents.h" +#include "url/gurl.h" + +#if defined(OS_ANDROID) +#include "components/site_engagement/content/android/site_engagement_service_android.h" +#endif + +namespace site_engagement { + +namespace { + +// Global bool to ensure we only update the parameters from variations once. +bool g_updated_from_variations = false; + +SiteEngagementService::ServiceProvider* g_service_provider = nullptr; + +// Length of time between metrics logging. +const int kMetricsIntervalInMinutes = 60; + +// A clock that keeps showing the time it was constructed with. +class StoppedClock : public base::Clock { + public: + explicit StoppedClock(base::Time time) : time_(time) {} + ~StoppedClock() override = default; + + protected: + // base::Clock: + base::Time Now() const override { return time_; } + + private: + const base::Time time_; + + DISALLOW_COPY_AND_ASSIGN(StoppedClock); +}; + +// Helpers for fetching content settings for one type. +ContentSettingsForOneType GetContentSettingsFromMap(HostContentSettingsMap* map, + ContentSettingsType type) { + ContentSettingsForOneType content_settings; + map->GetSettingsForOneType(type, &content_settings); + return content_settings; +} + +ContentSettingsForOneType GetContentSettingsFromBrowserContext( + content::BrowserContext* browser_context, + ContentSettingsType type) { + return GetContentSettingsFromMap( + permissions::PermissionsClient::Get()->GetSettingsMap(browser_context), + type); +} + +// Returns the combined list of origins which either have site engagement +// data stored, or have other settings that would provide a score bonus. +std::set<GURL> GetEngagementOriginsFromContentSettings( + HostContentSettingsMap* map) { + std::set<GURL> urls; + + // Fetch URLs of sites with engagement details stored. + for (const auto& site : + GetContentSettingsFromMap(map, ContentSettingsType::SITE_ENGAGEMENT)) { + urls.insert(GURL(site.primary_pattern.ToString())); + } + + return urls; +} + +SiteEngagementScore CreateEngagementScoreImpl(base::Clock* clock, + const GURL& origin, + HostContentSettingsMap* map) { + return SiteEngagementScore(clock, origin, map); +} + +mojom::SiteEngagementDetails GetDetailsImpl(base::Clock* clock, + const GURL& origin, + HostContentSettingsMap* map) { + return CreateEngagementScoreImpl(clock, origin, map).GetDetails(); +} + +std::vector<mojom::SiteEngagementDetails> GetAllDetailsImpl( + browsing_data::TimePeriod time_period, + base::Clock* clock, + HostContentSettingsMap* map) { + std::set<GURL> origins = GetEngagementOriginsFromContentSettings(map); + + std::vector<mojom::SiteEngagementDetails> details; + details.reserve(origins.size()); + + auto begin_time = browsing_data::CalculateBeginDeleteTime(time_period); + auto end_time = browsing_data::CalculateEndDeleteTime(time_period); + + for (const GURL& origin : origins) { + if (!origin.is_valid()) + continue; + + auto score = CreateEngagementScoreImpl(clock, origin, map); + auto last_engagement_time = score.last_engagement_time(); + if (begin_time > last_engagement_time || end_time < last_engagement_time) + continue; + + details.push_back(score.GetDetails()); + } + + return details; +} + +// Only accept a navigation event for engagement if it is one of: +// a. direct typed navigation +// b. clicking on an omnibox suggestion brought up by typing a keyword +// c. clicking on a bookmark or opening a bookmark app +// d. a custom search engine keyword search (e.g. Wikipedia search box added as +// search engine) +// e. an automatically generated top level navigation (e.g. command line +// navigation, in product help link). +bool IsEngagementNavigation(ui::PageTransition transition) { + return ui::PageTransitionCoreTypeIs(transition, ui::PAGE_TRANSITION_TYPED) || + ui::PageTransitionCoreTypeIs(transition, + ui::PAGE_TRANSITION_GENERATED) || + ui::PageTransitionCoreTypeIs(transition, + ui::PAGE_TRANSITION_AUTO_BOOKMARK) || + ui::PageTransitionCoreTypeIs(transition, + ui::PAGE_TRANSITION_KEYWORD_GENERATED) || + ui::PageTransitionCoreTypeIs(transition, + ui::PAGE_TRANSITION_AUTO_TOPLEVEL); +} + +} // namespace + +const char SiteEngagementService::kEngagementParams[] = "SiteEngagement"; + +// static +void SiteEngagementService::RegisterProfilePrefs(PrefRegistrySimple* registry) { + registry->RegisterInt64Pref(prefs::kSiteEngagementLastUpdateTime, 0, + PrefRegistry::LOSSY_PREF); +} + +// static +SiteEngagementService* SiteEngagementService::Get( + content::BrowserContext* context) { + DCHECK(g_service_provider); + return g_service_provider->GetSiteEngagementService(context); +} + +// static +void SiteEngagementService::SetServiceProvider(ServiceProvider* provider) { + DCHECK(provider); + DCHECK(!g_service_provider); + g_service_provider = provider; +} + +// static +void SiteEngagementService::ClearServiceProvider(ServiceProvider* provider) { + DCHECK(provider); + DCHECK_EQ(provider, g_service_provider); + g_service_provider = nullptr; +} + +// static +double SiteEngagementService::GetMaxPoints() { + return SiteEngagementScore::kMaxPoints; +} + +// static +bool SiteEngagementService::IsEnabled() { + const std::string group_name = + base::FieldTrialList::FindFullName(kEngagementParams); + return !base::StartsWith(group_name, "Disabled", + base::CompareCase::SENSITIVE); +} + +// static +double SiteEngagementService::GetScoreFromSettings( + HostContentSettingsMap* settings, + const GURL& origin) { + return SiteEngagementScore(base::DefaultClock::GetInstance(), origin, + settings) + .GetTotalScore(); +} + +// static +std::vector<mojom::SiteEngagementDetails> +SiteEngagementService::GetAllDetailsInBackground( + base::Time now, + scoped_refptr<HostContentSettingsMap> map) { + StoppedClock clock(now); + return GetAllDetailsImpl(browsing_data::TimePeriod::ALL_TIME, &clock, + map.get()); +} + +SiteEngagementService::SiteEngagementService(content::BrowserContext* context) + : browser_context_(context), clock_(base::DefaultClock::GetInstance()) { + content::GetUIThreadTaskRunner({base::TaskPriority::BEST_EFFORT}) + ->PostTask(FROM_HERE, + base::BindOnce(&SiteEngagementService::AfterStartupTask, + weak_factory_.GetWeakPtr())); + + if (!g_updated_from_variations) { + SiteEngagementScore::UpdateFromVariations(kEngagementParams); + g_updated_from_variations = true; + } +} + +SiteEngagementService::~SiteEngagementService() { + // Clear any observers to avoid dangling pointers back to this object. + for (auto& observer : observer_list_) + observer.Observe(nullptr); +} + +blink::mojom::EngagementLevel SiteEngagementService::GetEngagementLevel( + const GURL& url) const { + if (IsLastEngagementStale()) + CleanupEngagementScores(true); + + return CreateEngagementScore(url).GetEngagementLevel(); +} + +std::vector<mojom::SiteEngagementDetails> SiteEngagementService::GetAllDetails() + const { + if (IsLastEngagementStale()) + CleanupEngagementScores(true); + + return GetAllDetailsImpl( + browsing_data::TimePeriod::ALL_TIME, clock_, + permissions::PermissionsClient::Get()->GetSettingsMap(browser_context_)); +} + +std::vector<mojom::SiteEngagementDetails> +SiteEngagementService::GetAllDetailsEngagedInTimePeriod( + browsing_data::TimePeriod time_period) const { + if (IsLastEngagementStale()) + CleanupEngagementScores(true); + + return GetAllDetailsImpl( + time_period, clock_, + permissions::PermissionsClient::Get()->GetSettingsMap(browser_context_)); +} + +void SiteEngagementService::HandleNotificationInteraction(const GURL& url) { + if (!ShouldRecordEngagement(url)) + return; + + AddPoints(url, SiteEngagementScore::GetNotificationInteractionPoints()); + + MaybeRecordMetrics(); + OnEngagementEvent(nullptr /* web_contents */, url, + EngagementType::kNotificationInteraction); +} + +bool SiteEngagementService::IsBootstrapped() const { + return GetTotalEngagementPoints() >= + SiteEngagementScore::GetBootstrapPoints(); +} + +bool SiteEngagementService::IsEngagementAtLeast( + const GURL& url, + blink::mojom::EngagementLevel level) const { + DCHECK_LT(SiteEngagementScore::GetMediumEngagementBoundary(), + SiteEngagementScore::GetHighEngagementBoundary()); + double score = GetScore(url); + switch (level) { + case blink::mojom::EngagementLevel::NONE: + return true; + case blink::mojom::EngagementLevel::MINIMAL: + return score > 0; + case blink::mojom::EngagementLevel::LOW: + return score >= 1; + case blink::mojom::EngagementLevel::MEDIUM: + return score >= SiteEngagementScore::GetMediumEngagementBoundary(); + case blink::mojom::EngagementLevel::HIGH: + return score >= SiteEngagementScore::GetHighEngagementBoundary(); + case blink::mojom::EngagementLevel::MAX: + return score == SiteEngagementScore::kMaxPoints; + } + NOTREACHED(); + return false; +} + +void SiteEngagementService::AddObserver(SiteEngagementObserver* observer) { + observer_list_.AddObserver(observer); +} + +void SiteEngagementService::RemoveObserver(SiteEngagementObserver* observer) { + observer_list_.RemoveObserver(observer); +} + +void SiteEngagementService::ResetBaseScoreForURL(const GURL& url, + double score) { + SiteEngagementScore engagement_score = CreateEngagementScore(url); + engagement_score.Reset(score, clock_->Now()); + engagement_score.Commit(); +} + +void SiteEngagementService::SetLastShortcutLaunchTime( + content::WebContents* web_contents, + const GURL& url) { + SiteEngagementScore score = CreateEngagementScore(url); + + // Record the number of days since the last launch in UMA. If the user's clock + // has changed back in time, set this to 0. + base::Time now = clock_->Now(); + base::Time last_launch = score.last_shortcut_launch_time(); + if (!last_launch.is_null()) { + SiteEngagementMetrics::RecordDaysSinceLastShortcutLaunch( + std::max(0, (now - last_launch).InDays())); + } + + score.set_last_shortcut_launch_time(now); + score.Commit(); + + OnEngagementEvent(web_contents, url, EngagementType::kWebappShortcutLaunch); +} + +double SiteEngagementService::GetScore(const GURL& url) const { + return GetDetails(url).total_score; +} + +mojom::SiteEngagementDetails SiteEngagementService::GetDetails( + const GURL& url) const { + // Ensure that if engagement is stale, we clean things up before fetching the + // score. + if (IsLastEngagementStale()) + CleanupEngagementScores(true); + + return GetDetailsImpl( + clock_, url, + permissions::PermissionsClient::Get()->GetSettingsMap(browser_context_)); +} + +double SiteEngagementService::GetTotalEngagementPoints() const { + std::vector<mojom::SiteEngagementDetails> details = GetAllDetails(); + + double total_score = 0; + for (const auto& detail : details) + total_score += detail.total_score; + + return total_score; +} + +void SiteEngagementService::AddPointsForTesting(const GURL& url, + double points) { + AddPoints(url, points); +} + +#if defined(OS_ANDROID) +SiteEngagementServiceAndroid* SiteEngagementService::GetAndroidService() const { + return android_service_.get(); +} + +void SiteEngagementService::SetAndroidService( + std::unique_ptr<SiteEngagementServiceAndroid> android_service) { + android_service_ = std::move(android_service); +} +#endif + +void SiteEngagementService::AddPoints(const GURL& url, double points) { + if (points == 0) + return; + + // Trigger a cleanup and date adjustment if it has been a substantial length + // of time since *any* engagement was recorded by the service. This will + // ensure that we do not decay scores when the user did not use the browser. + if (IsLastEngagementStale()) + CleanupEngagementScores(true); + + SiteEngagementScore score = CreateEngagementScore(url); + score.AddPoints(points); + score.Commit(); + + SetLastEngagementTime(score.last_engagement_time()); +} + +void SiteEngagementService::AfterStartupTask() { + // Check if we need to reset last engagement times on startup - we want to + // avoid doing this in AddPoints() if possible. It is still necessary to check + // in AddPoints for people who never restart Chrome, but leave it open and + // their computer on standby. + CleanupEngagementScores(IsLastEngagementStale()); +} + +void SiteEngagementService::CleanupEngagementScores( + bool update_last_engagement_time) const { + TRACE_EVENT0("navigation", "SiteEngagementService::CleanupEngagementScores"); + + // We want to rebase last engagement times relative to MaxDecaysPerScore + // periods of decay in the past. + base::Time now = clock_->Now(); + base::Time last_engagement_time = GetLastEngagementTime(); + base::Time rebase_time = now - GetMaxDecayPeriod(); + base::Time new_last_engagement_time; + + // If |update_last_engagement_time| is true, we must have either: + // a) last_engagement_time is in the future; OR + // b) last_engagement_time < rebase_time < now + DCHECK(!update_last_engagement_time || last_engagement_time >= now || + (last_engagement_time < rebase_time && rebase_time < now)); + + // Cap |last_engagement_time| at |now| if it is in the future. This ensures + // that we use sane offsets when a user has adjusted their clock backwards and + // have a mix of scores prior to and after |now|. + if (last_engagement_time > now) + last_engagement_time = now; + + HostContentSettingsMap* settings_map = + permissions::PermissionsClient::Get()->GetSettingsMap(browser_context_); + for (const auto& site : GetContentSettingsFromBrowserContext( + browser_context_, ContentSettingsType::SITE_ENGAGEMENT)) { + GURL origin(site.primary_pattern.ToString()); + + if (origin.is_valid()) { + SiteEngagementScore score = CreateEngagementScore(origin); + if (update_last_engagement_time) { + // Catch cases of users moving their clocks, or a potential race where + // a score content setting is written out to prefs, but the updated + // |last_engagement_time| was not written, as both are lossy + // preferences. |rebase_time| is strictly in the past, so any score with + // a last updated time in the future is caught by this branch. + if (score.last_engagement_time() > rebase_time) { + score.set_last_engagement_time(now); + } else if (score.last_engagement_time() > last_engagement_time) { + // This score is newer than |last_engagement_time|, but older than + // |rebase_time|. It should still be rebased with no offset as we + // don't accurately know what the offset should be. + score.set_last_engagement_time(rebase_time); + } else { + // Work out the offset between this score's last engagement time and + // the last time the service recorded any engagement. Set the score's + // last engagement time to rebase_time - offset to preserve its state, + // relative to the rebase date. This ensures that the score will decay + // the next time it is used, but will not decay too much. + base::TimeDelta offset = + last_engagement_time - score.last_engagement_time(); + base::Time rebase_score_time = rebase_time - offset; + score.set_last_engagement_time(rebase_score_time); + } + + if (score.last_engagement_time() > new_last_engagement_time) + new_last_engagement_time = score.last_engagement_time(); + score.Commit(); + } + + if (score.GetTotalScore() > + SiteEngagementScore::GetScoreCleanupThreshold()) + continue; + } + + // This origin has a score of 0. Wipe it from content settings. + settings_map->SetWebsiteSettingDefaultScope( + origin, GURL(), ContentSettingsType::SITE_ENGAGEMENT, nullptr); + } + + // Set the last engagement time to be consistent with the scores. This will + // only occur if |update_last_engagement_time| is true. + if (!new_last_engagement_time.is_null()) + SetLastEngagementTime(new_last_engagement_time); +} + +void SiteEngagementService::MaybeRecordMetrics() { + base::Time now = clock_->Now(); + if (browser_context_->IsOffTheRecord() || + (!last_metrics_time_.is_null() && + (now - last_metrics_time_).InMinutes() < kMetricsIntervalInMinutes)) { + return; + } + + // Clean up engagement first before retrieving scores. + if (IsLastEngagementStale()) + CleanupEngagementScores(true); + + last_metrics_time_ = now; + + // Retrieve details on a background thread as this is expensive. We may end up + // with minor data inconsistency but this doesn't really matter for metrics + // purposes. + // + // The BrowserContext and its KeyedServices are normally destroyed before the + // ThreadPool shuts down background threads, so the task needs to hold a + // strong reference to HostContentSettingsMap (which supports outliving the + // browser context), and needs to avoid using any members of + // SiteEngagementService (which does not). See https://crbug.com/900022. + base::ThreadPool::PostTaskAndReplyWithResult( + FROM_HERE, + {base::TaskPriority::BEST_EFFORT, + base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}, + base::BindOnce(&GetAllDetailsInBackground, now, + base::WrapRefCounted( + permissions::PermissionsClient::Get()->GetSettingsMap( + browser_context_))), + base::BindOnce(&SiteEngagementService::RecordMetrics, + weak_factory_.GetWeakPtr())); +} + +void SiteEngagementService::RecordMetrics( + std::vector<mojom::SiteEngagementDetails> details) { + TRACE_EVENT0("navigation", "SiteEngagementService::RecordMetrics"); + std::sort(details.begin(), details.end(), + [](const mojom::SiteEngagementDetails& lhs, + const mojom::SiteEngagementDetails& rhs) { + return lhs.total_score < rhs.total_score; + }); + + int total_origins = details.size(); + + double total_engagement = 0; + int origins_with_max_engagement = 0; + for (const auto& detail : details) { + if (detail.total_score == SiteEngagementScore::kMaxPoints) + ++origins_with_max_engagement; + total_engagement += detail.total_score; + } + + double mean_engagement = + (total_origins == 0 ? 0 : total_engagement / total_origins); + + SiteEngagementMetrics::RecordTotalOriginsEngaged(total_origins); + SiteEngagementMetrics::RecordTotalSiteEngagement(total_engagement); + SiteEngagementMetrics::RecordMeanEngagement(mean_engagement); + SiteEngagementMetrics::RecordMedianEngagement( + GetMedianEngagementFromSortedDetails(details)); + SiteEngagementMetrics::RecordEngagementScores(details); + + SiteEngagementMetrics::RecordOriginsWithMaxDailyEngagement( + OriginsWithMaxDailyEngagement()); + SiteEngagementMetrics::RecordOriginsWithMaxEngagement( + origins_with_max_engagement); +} + +bool SiteEngagementService::ShouldRecordEngagement(const GURL& url) const { + return url.SchemeIsHTTPOrHTTPS(); +} + +base::Time SiteEngagementService::GetLastEngagementTime() const { + if (browser_context_->IsOffTheRecord()) + return base::Time(); + + return base::Time::FromInternalValue( + user_prefs::UserPrefs::Get(browser_context_) + ->GetInt64(prefs::kSiteEngagementLastUpdateTime)); +} + +void SiteEngagementService::SetLastEngagementTime( + base::Time last_engagement_time) const { + if (browser_context_->IsOffTheRecord()) + return; + user_prefs::UserPrefs::Get(browser_context_) + ->SetInt64(prefs::kSiteEngagementLastUpdateTime, + last_engagement_time.ToInternalValue()); +} + +base::TimeDelta SiteEngagementService::GetMaxDecayPeriod() const { + return base::TimeDelta::FromHours( + SiteEngagementScore::GetDecayPeriodInHours()) * + SiteEngagementScore::GetMaxDecaysPerScore(); +} + +base::TimeDelta SiteEngagementService::GetStalePeriod() const { + return GetMaxDecayPeriod() + + base::TimeDelta::FromHours( + SiteEngagementScore::GetLastEngagementGracePeriodInHours()); +} + +double SiteEngagementService::GetMedianEngagementFromSortedDetails( + const std::vector<mojom::SiteEngagementDetails>& details) const { + if (details.empty()) + return 0; + + // Calculate the median as the middle value of the sorted engagement scores + // if there are an odd number of scores, or the average of the two middle + // scores otherwise. + size_t mid = details.size() / 2; + if (details.size() % 2 == 1) + return details[mid].total_score; + else + return (details[mid - 1].total_score + details[mid].total_score) / 2; +} + +void SiteEngagementService::HandleMediaPlaying( + content::WebContents* web_contents, + bool is_hidden) { + const GURL& url = web_contents->GetLastCommittedURL(); + if (!ShouldRecordEngagement(url)) + return; + + AddPoints(url, is_hidden ? SiteEngagementScore::GetHiddenMediaPoints() + : SiteEngagementScore::GetVisibleMediaPoints()); + + MaybeRecordMetrics(); + OnEngagementEvent( + web_contents, url, + is_hidden ? EngagementType::kMediaHidden : EngagementType::kMediaVisible); +} + +void SiteEngagementService::HandleNavigation(content::WebContents* web_contents, + ui::PageTransition transition) { + const GURL& url = web_contents->GetLastCommittedURL(); + if (!IsEngagementNavigation(transition) || !ShouldRecordEngagement(url)) + return; + + AddPoints(url, SiteEngagementScore::GetNavigationPoints()); + + MaybeRecordMetrics(); + OnEngagementEvent(web_contents, url, EngagementType::kNavigation); +} + +void SiteEngagementService::HandleUserInput(content::WebContents* web_contents, + EngagementType type) { + const GURL& url = web_contents->GetLastCommittedURL(); + if (!ShouldRecordEngagement(url)) + return; + + AddPoints(url, SiteEngagementScore::GetUserInputPoints()); + + MaybeRecordMetrics(); + OnEngagementEvent(web_contents, url, type); +} + +void SiteEngagementService::OnEngagementEvent( + content::WebContents* web_contents, + const GURL& url, + EngagementType type) { + SiteEngagementMetrics::RecordEngagement(type); + + double score = GetScore(url); + for (SiteEngagementObserver& observer : observer_list_) + observer.OnEngagementEvent(web_contents, url, score, type); +} + +bool SiteEngagementService::IsLastEngagementStale() const { + // |last_engagement_time| will be null when no engagement has been recorded + // (first run or post clearing site data), or if we are running in incognito. + // Do not regard these cases as stale. + base::Time last_engagement_time = GetLastEngagementTime(); + if (last_engagement_time.is_null()) + return false; + + // Stale is either too *far* back, or any amount *forward* in time. This could + // occur due to a changed clock, or extended non-use of the browser. + base::Time now = clock_->Now(); + return (now - last_engagement_time) >= GetStalePeriod() || + (now < last_engagement_time); +} + +SiteEngagementScore SiteEngagementService::CreateEngagementScore( + const GURL& 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 CreateEngagementScoreImpl( + clock_, origin, + permissions::PermissionsClient::Get()->GetSettingsMap(browser_context_)); +} + +int SiteEngagementService::OriginsWithMaxDailyEngagement() const { + int total_origins = 0; + + // We cannot call GetScoreMap as we need the score objects, not raw scores. + for (const auto& site : GetContentSettingsFromBrowserContext( + browser_context_, ContentSettingsType::SITE_ENGAGEMENT)) { + GURL origin(site.primary_pattern.ToString()); + + if (!origin.is_valid()) + continue; + + if (CreateEngagementScore(origin).MaxPointsPerDayAdded()) + ++total_origins; + } + + return total_origins; +} + +} // namespace site_engagement diff --git a/chromium/components/site_engagement/content/site_engagement_service.h b/chromium/components/site_engagement/content/site_engagement_service.h new file mode 100644 index 00000000000..79e789afaab --- /dev/null +++ b/chromium/components/site_engagement/content/site_engagement_service.h @@ -0,0 +1,341 @@ +// Copyright 2015 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. + +#ifndef COMPONENTS_SITE_ENGAGEMENT_CONTENT_SITE_ENGAGEMENT_SERVICE_H_ +#define COMPONENTS_SITE_ENGAGEMENT_CONTENT_SITE_ENGAGEMENT_SERVICE_H_ + +#include <memory> +#include <vector> + +#include "base/gtest_prod_util.h" +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "base/observer_list.h" +#include "base/time/time.h" +#include "build/build_config.h" +#include "components/browsing_data/core/browsing_data_utils.h" +#include "components/keyed_service/core/keyed_service.h" +#include "components/site_engagement/core/mojom/site_engagement_details.mojom.h" +#include "third_party/blink/public/mojom/site_engagement/site_engagement.mojom.h" +#include "ui/base/page_transition_types.h" + +namespace base { +class Clock; +} + +namespace webapps { +FORWARD_DECLARE_TEST(AppBannerManagerBrowserTest, WebAppBannerNeedsEngagement); +} + +namespace content { +class BrowserContext; +class WebContents; +} // namespace content + +namespace web_app { +class WebAppEngagementBrowserTest; +} + +class GURL; +class HostContentSettingsMap; +class PrefRegistrySimple; + +namespace site_engagement { + +enum class EngagementType; +class SiteEngagementObserver; +class SiteEngagementScore; + +#if defined(OS_ANDROID) +class SiteEngagementServiceAndroid; +#endif + +class SiteEngagementScoreProvider { + public: + // Returns a non-negative integer representing the engagement score of the + // origin for this URL. + virtual double GetScore(const GURL& url) const = 0; + + // Returns the sum of engagement points awarded to all sites. + virtual double GetTotalEngagementPoints() const = 0; +}; + +// Stores and retrieves the engagement score of an origin. +// +// An engagement score is a non-negative double that represents how much a user +// has engaged with an origin - the higher it is, the more engagement the user +// has had with this site recently. +// +// User activity such as visiting the origin often, interacting with the origin, +// and adding it to the homescreen will increase the site engagement score. If +// a site's score does not increase for some time, it will decay, eventually +// reaching zero with further disuse. +// +// The SiteEngagementService object must be created and used on the UI thread +// only. Engagement scores may be queried in a read-only fashion from other +// threads using SiteEngagementService::GetScoreFromSettings, but use of this +// method is discouraged unless it is not possible to use the UI thread. +class SiteEngagementService : public KeyedService, + public SiteEngagementScoreProvider { + public: + // The provider allows code agnostic to the embedder (e.g. in + // //components) to retrieve the SiteEngagementService. It should be set by + // each embedder that uses the SiteEngagementService, via SetServiceProvider. + class ServiceProvider { + public: + ~ServiceProvider() = default; + + // Should always return a non null value, creating the service if it does + // not exist. + virtual SiteEngagementService* GetSiteEngagementService( + content::BrowserContext* browser_context) = 0; + }; + + // WebContentsObserver that detects engagement triggering events and notifies + // the service of them. + class Helper; + + // The name of the site engagement variation field trial. + static const char kEngagementParams[]; + + static void RegisterProfilePrefs(PrefRegistrySimple* registry); + + // Sets and clears the service provider. These are separate functions to + // enable better checking. + static void SetServiceProvider(ServiceProvider* provider); + static void ClearServiceProvider(ServiceProvider* provider); + + // Returns the site engagement service attached to this Browser Context. The + // service exists in incognito mode; scores will be initialised using the + // score from the Browser Context that the incognito session was created from, + // and will increase and decrease as usual. Engagement earned or decayed in + // incognito will not be persisted or reflected in the original Browser + // Context. + // + // This method must be called on the UI thread. + static SiteEngagementService* Get(content::BrowserContext* browser_context); + + // Returns the maximum possible amount of engagement that a site can accrue. + static double GetMaxPoints(); + + // Returns whether or not the site engagement service is enabled. + static bool IsEnabled(); + + // Returns the score for |origin| based on |settings|. Can be called on any + // thread and does not cause any cleanup, decay, etc. + // + // Should only be used if you cannot create a SiteEngagementService (i.e. you + // cannot run on the UI thread). + static double GetScoreFromSettings(HostContentSettingsMap* settings, + const GURL& origin); + + // Retrieves all details. Can be called from a background thread. |now| must + // be the current timestamp. Takes a scoped_refptr to keep + // HostContentSettingsMap alive. See crbug.com/901287. + static std::vector<mojom::SiteEngagementDetails> GetAllDetailsInBackground( + base::Time now, + scoped_refptr<HostContentSettingsMap> map); + + explicit SiteEngagementService(content::BrowserContext* browser_context); + ~SiteEngagementService() override; + + // Returns the engagement level of |url|. + blink::mojom::EngagementLevel GetEngagementLevel(const GURL& url) const; + + // Returns an array of engagement score details for all origins which have + // a score, whether due to direct engagement, or other factors that cause + // an engagement bonus to be applied. + // + // Note that this method is quite expensive, so try to avoid calling it in + // performance-critical code. + std::vector<mojom::SiteEngagementDetails> GetAllDetails() const; + + // Return an array of engagement score details for all origins which have + // had engagement since the specified time. + // + // Note that this method is quite expensive, so try to avoid calling it in + // performance-critical code. + std::vector<mojom::SiteEngagementDetails> GetAllDetailsEngagedInTimePeriod( + browsing_data::TimePeriod time_period) const; + + // Update the engagement score of |url| for a notification interaction. + void HandleNotificationInteraction(const GURL& url); + + // Returns whether the engagement service has enough data to make meaningful + // decisions. Clients should avoid using engagement in their heuristic until + // this is true. + bool IsBootstrapped() const; + + // Returns whether |url| has at least the given |level| of engagement. + bool IsEngagementAtLeast(const GURL& url, + blink::mojom::EngagementLevel level) const; + + // Resets the base engagement for |url| to |score|, clearing daily limits. Any + // bonus engagement that |url| has acquired is not affected by this method, so + // the result of GetScore(|url|) may not be the same as |score|. + void ResetBaseScoreForURL(const GURL& url, double score); + + // Update the last time |url| was opened from an installed shortcut (hosted in + // |web_contents|) to be clock_->Now(). + void SetLastShortcutLaunchTime(content::WebContents* web_contents, + const GURL& url); + + // Returns the site engagement details for the specified |url|. + mojom::SiteEngagementDetails GetDetails(const GURL& url) const; + + // Overridden from SiteEngagementScoreProvider. + double GetScore(const GURL& url) const override; + double GetTotalEngagementPoints() const override; + + // Just forwards calls AddPoints. + void AddPointsForTesting(const GURL& url, double points); + + void SetClockForTesting(base::Clock* clock) { clock_ = clock; } + + protected: + // Retrieves the SiteEngagementScore object for |origin|. + SiteEngagementScore CreateEngagementScore(const GURL& origin) const; + void SetLastEngagementTime(base::Time last_engagement_time) const; + + content::BrowserContext* browser_context() { return browser_context_; } + const base::Clock& clock() { return *clock_; } + + private: + friend class SiteEngagementObserver; + friend class SiteEngagementServiceTest; + friend class web_app::WebAppEngagementBrowserTest; + FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, CheckHistograms); + FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, CleanupEngagementScores); + FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, + CleanupMovesScoreBackToNow); + FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, + CleanupMovesScoreBackToRebase); + FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, + CleanupEngagementScoresProportional); + FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, GetTotalNavigationPoints); + FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, GetTotalUserInputPoints); + FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, RestrictedToHTTPAndHTTPS); + FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, Observers); + FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, LastEngagementTime); + FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, + IncognitoEngagementService); + FRIEND_TEST_ALL_PREFIXES(webapps::AppBannerManagerBrowserTest, + WebAppBannerNeedsEngagement); + FRIEND_TEST_ALL_PREFIXES(AppBannerSettingsHelperTest, SiteEngagementTrigger); + FRIEND_TEST_ALL_PREFIXES(HostedAppPWAOnlyTest, EngagementHistogram); + +#if defined(OS_ANDROID) + // Shim class to expose the service to Java. + friend class SiteEngagementServiceAndroid; + SiteEngagementServiceAndroid* GetAndroidService() const; + void SetAndroidService( + std::unique_ptr<SiteEngagementServiceAndroid> android_service); +#endif + + // Adds the specified number of points to the given origin, respecting the + // maximum limits for the day and overall. + void AddPoints(const GURL& url, double points); + + // Runs site engagement maintenance tasks. + void AfterStartupTask(); + + // Removes any origins which have decayed to 0 engagement. If + // |update_last_engagement_time| is true, the last engagement time of all + // origins is reset by calculating the delta between the last engagement event + // recorded by the site engagement service and the origin. The origin's last + // engagement time is then set to clock_->Now() - delta. + // + // If a user does not use the browser at all for some period of time, + // engagement is not decayed, and the state is restored equivalent to how they + // left it once they return. + void CleanupEngagementScores(bool update_last_engagement_time) const; + + // Possibly records UMA metrics if we haven't recorded them lately. + void MaybeRecordMetrics(); + + // Actually records metrics for the engagement in |details|. + void RecordMetrics(std::vector<mojom::SiteEngagementDetails>); + + // Returns true if we should record engagement for this URL. Currently, + // engagement is only earned for HTTP and HTTPS. + bool ShouldRecordEngagement(const GURL& url) const; + + // Get and set the last engagement time from prefs. + base::Time GetLastEngagementTime() const; + + // Get the maximum decay period and the stale period for last engagement + // times. + base::TimeDelta GetMaxDecayPeriod() const; + base::TimeDelta GetStalePeriod() const; + + // Returns the median engagement score of all recorded origins. |details| must + // be sorted in ascending order of score. + double GetMedianEngagementFromSortedDetails( + const std::vector<mojom::SiteEngagementDetails>& details) const; + + // Update the engagement score of the origin loaded in |web_contents| for + // media playing. The points awarded are discounted if the media is being + // played in a non-visible tab. + void HandleMediaPlaying(content::WebContents* web_contents, bool is_hidden); + + // Update the engagement score of the origin loaded in |web_contents| for + // navigation. + void HandleNavigation(content::WebContents* web_contents, + ui::PageTransition transition); + + // Update the engagement score of the origin loaded in |web_contents| for + // time-on-site, based on user input. + void HandleUserInput(content::WebContents* web_contents, EngagementType type); + + // Called when the engagement for |url| loaded in |web_contents| is changed, + // due to an event of type |type|. Calls OnEngagementEvent in all observers. + // |web_contents| may be null if the engagement has increased when |url| is + // not in a tab, e.g. from a notification interaction. Also records + // engagement-type metrics. + void OnEngagementEvent(content::WebContents* web_contents, + const GURL& url, + EngagementType type); + + // Returns true if the last engagement increasing event seen by the site + // engagement service was sufficiently long ago that we need to reset all + // scores to be relative to now. This ensures that users who do not use the + // browser for an extended period of time do not have their engagement decay. + bool IsLastEngagementStale() const; + + // Returns the number of origins with maximum daily and total engagement + // respectively. + int OriginsWithMaxDailyEngagement() const; + + // Add and remove observers of this service. + void AddObserver(SiteEngagementObserver* observer); + void RemoveObserver(SiteEngagementObserver* observer); + + content::BrowserContext* browser_context_; + + // The clock used to vend times. + base::Clock* clock_; + +#if defined(OS_ANDROID) + std::unique_ptr<SiteEngagementServiceAndroid> android_service_; +#endif + + // Metrics are recorded at non-incognito browser startup, and then + // approximately once per hour thereafter. Store the local time at which + // metrics were previously uploaded: the first event which affects any + // origin's engagement score after an hour has elapsed triggers the next + // upload. + base::Time last_metrics_time_; + + // A list of observers. When any origin registers an engagement-increasing + // event, each observer's OnEngagementEvent method will be called. + base::ObserverList<SiteEngagementObserver>::Unchecked observer_list_; + + base::WeakPtrFactory<SiteEngagementService> weak_factory_{this}; + + DISALLOW_COPY_AND_ASSIGN(SiteEngagementService); +}; + +} // namespace site_engagement + +#endif // COMPONENTS_SITE_ENGAGEMENT_CONTENT_SITE_ENGAGEMENT_SERVICE_H_ diff --git a/chromium/components/site_engagement/core/BUILD.gn b/chromium/components/site_engagement/core/BUILD.gn new file mode 100644 index 00000000000..dbf7d358921 --- /dev/null +++ b/chromium/components/site_engagement/core/BUILD.gn @@ -0,0 +1,10 @@ +# Copyright 2020 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. + +static_library("core") { + sources = [ + "pref_names.cc", + "pref_names.h", + ] +} diff --git a/chromium/components/site_engagement/core/mojom/site_engagement_details.mojom b/chromium/components/site_engagement/core/mojom/site_engagement_details.mojom index 42549b019ea..5cbaa5bf657 100644 --- a/chromium/components/site_engagement/core/mojom/site_engagement_details.mojom +++ b/chromium/components/site_engagement/core/mojom/site_engagement_details.mojom @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -module mojom; +module site_engagement.mojom; import "url/mojom/url.mojom"; diff --git a/chromium/components/site_engagement/core/pref_names.cc b/chromium/components/site_engagement/core/pref_names.cc new file mode 100644 index 00000000000..37674836481 --- /dev/null +++ b/chromium/components/site_engagement/core/pref_names.cc @@ -0,0 +1,17 @@ +// Copyright 2020 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 "components/site_engagement/core/pref_names.h" + +namespace site_engagement { +namespace prefs { + +// The last time that the site engagement service recorded an engagement event +// for this profile for any URL. Recorded only during shutdown. Used to prevent +// the service from decaying engagement when a user does not use the browser at +// all for an extended period of time. +const char kSiteEngagementLastUpdateTime[] = "profile.last_engagement_time"; + +} // namespace prefs +} // namespace site_engagement diff --git a/chromium/components/site_engagement/core/pref_names.h b/chromium/components/site_engagement/core/pref_names.h new file mode 100644 index 00000000000..97a3f996645 --- /dev/null +++ b/chromium/components/site_engagement/core/pref_names.h @@ -0,0 +1,16 @@ +// Copyright 2020 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. + +#ifndef COMPONENTS_SITE_ENGAGEMENT_CORE_PREF_NAMES_H_ +#define COMPONENTS_SITE_ENGAGEMENT_CORE_PREF_NAMES_H_ + +namespace site_engagement { +namespace prefs { + +extern const char kSiteEngagementLastUpdateTime[]; + +} // namespace prefs +} // namespace site_engagement + +#endif // COMPONENTS_SITE_ENGAGEMENT_CORE_PREF_NAMES_H_ |
