summaryrefslogtreecommitdiff
path: root/chromium/components/site_engagement
diff options
context:
space:
mode:
authorAllan Sandfeld Jensen <allan.jensen@qt.io>2021-05-20 09:47:09 +0200
committerAllan Sandfeld Jensen <allan.jensen@qt.io>2021-06-07 11:15:42 +0000
commit189d4fd8fad9e3c776873be51938cd31a42b6177 (patch)
tree6497caeff5e383937996768766ab3bb2081a40b2 /chromium/components/site_engagement
parent8bc75099d364490b22f43a7ce366b366c08f4164 (diff)
downloadqtwebengine-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')
-rw-r--r--chromium/components/site_engagement/DIR_METADATA3
-rw-r--r--chromium/components/site_engagement/OWNERS2
-rw-r--r--chromium/components/site_engagement/content/BUILD.gn58
-rw-r--r--chromium/components/site_engagement/content/DEPS12
-rw-r--r--chromium/components/site_engagement/content/android/BUILD.gn19
-rw-r--r--chromium/components/site_engagement/content/android/DEPS3
-rw-r--r--chromium/components/site_engagement/content/android/site_engagement_service_android.cc85
-rw-r--r--chromium/components/site_engagement/content/android/site_engagement_service_android.h52
-rw-r--r--chromium/components/site_engagement/content/engagement_type.h31
-rw-r--r--chromium/components/site_engagement/content/site_engagement_metrics.cc135
-rw-r--r--chromium/components/site_engagement/content/site_engagement_metrics.h55
-rw-r--r--chromium/components/site_engagement/content/site_engagement_observer.cc40
-rw-r--r--chromium/components/site_engagement/content/site_engagement_observer.h59
-rw-r--r--chromium/components/site_engagement/content/site_engagement_score.cc407
-rw-r--r--chromium/components/site_engagement/content/site_engagement_score.h246
-rw-r--r--chromium/components/site_engagement/content/site_engagement_score_unittest.cc480
-rw-r--r--chromium/components/site_engagement/content/site_engagement_service.cc705
-rw-r--r--chromium/components/site_engagement/content/site_engagement_service.h341
-rw-r--r--chromium/components/site_engagement/core/BUILD.gn10
-rw-r--r--chromium/components/site_engagement/core/mojom/site_engagement_details.mojom2
-rw-r--r--chromium/components/site_engagement/core/pref_names.cc17
-rw-r--r--chromium/components/site_engagement/core/pref_names.h16
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, &param_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_