1 // Copyright 2015 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #ifndef CHROME_BROWSER_ENGAGEMENT_SITE_ENGAGEMENT_SERVICE_H_ 6 #define CHROME_BROWSER_ENGAGEMENT_SITE_ENGAGEMENT_SERVICE_H_ 7 8 #include <memory> 9 #include <set> 10 #include <vector> 11 12 #include "base/gtest_prod_util.h" 13 #include "base/macros.h" 14 #include "base/memory/weak_ptr.h" 15 #include "base/observer_list.h" 16 #include "base/time/time.h" 17 #include "build/build_config.h" 18 #include "components/browsing_data/core/browsing_data_utils.h" 19 #include "components/history/core/browser/history_service_observer.h" 20 #include "components/keyed_service/core/keyed_service.h" 21 #include "components/site_engagement/core/mojom/site_engagement_details.mojom.h" 22 #include "third_party/blink/public/mojom/site_engagement/site_engagement.mojom.h" 23 #include "ui/base/page_transition_types.h" 24 25 namespace base { 26 class Clock; 27 } 28 29 namespace banners { 30 FORWARD_DECLARE_TEST(AppBannerManagerBrowserTest, WebAppBannerNeedsEngagement); 31 } 32 33 namespace content { 34 class WebContents; 35 } 36 37 namespace history { 38 class HistoryService; 39 } 40 41 namespace web_app { 42 class WebAppEngagementBrowserTest; 43 } 44 45 class GURL; 46 class HostContentSettingsMap; 47 class Profile; 48 class SiteEngagementObserver; 49 class SiteEngagementScore; 50 51 #if defined(OS_ANDROID) 52 class SiteEngagementServiceAndroid; 53 #endif 54 55 class SiteEngagementScoreProvider { 56 public: 57 // Returns a non-negative integer representing the engagement score of the 58 // origin for this URL. 59 virtual double GetScore(const GURL& url) const = 0; 60 61 // Returns the sum of engagement points awarded to all sites. 62 virtual double GetTotalEngagementPoints() const = 0; 63 }; 64 65 // Stores and retrieves the engagement score of an origin. 66 // 67 // An engagement score is a non-negative double that represents how much a user 68 // has engaged with an origin - the higher it is, the more engagement the user 69 // has had with this site recently. 70 // 71 // User activity such as visiting the origin often, interacting with the origin, 72 // and adding it to the homescreen will increase the site engagement score. If 73 // a site's score does not increase for some time, it will decay, eventually 74 // reaching zero with further disuse. 75 // 76 // The SiteEngagementService object must be created and used on the UI thread 77 // only. Engagement scores may be queried in a read-only fashion from other 78 // threads using SiteEngagementService::GetScoreFromSettings, but use of this 79 // method is discouraged unless it is not possible to use the UI thread. 80 class SiteEngagementService : public KeyedService, 81 public history::HistoryServiceObserver, 82 public SiteEngagementScoreProvider { 83 public: 84 // This is used to back a UMA histogram, so it should be treated as 85 // append-only. Any new values should be inserted immediately prior to 86 // ENGAGEMENT_LAST and added to SiteEngagementServiceEngagementType in 87 // tools/metrics/histograms/enums.xml. 88 // TODO(calamity): Document each of these engagement types. 89 enum EngagementType { 90 ENGAGEMENT_NAVIGATION, 91 ENGAGEMENT_KEYPRESS, 92 ENGAGEMENT_MOUSE, 93 ENGAGEMENT_TOUCH_GESTURE, 94 ENGAGEMENT_SCROLL, 95 ENGAGEMENT_MEDIA_HIDDEN, 96 ENGAGEMENT_MEDIA_VISIBLE, 97 ENGAGEMENT_WEBAPP_SHORTCUT_LAUNCH, 98 ENGAGEMENT_FIRST_DAILY_ENGAGEMENT, 99 ENGAGEMENT_NOTIFICATION_INTERACTION, 100 ENGAGEMENT_LAST, 101 }; 102 103 // WebContentsObserver that detects engagement triggering events and notifies 104 // the service of them. 105 class Helper; 106 107 // The name of the site engagement variation field trial. 108 static const char kEngagementParams[]; 109 110 // Returns the site engagement service attached to this profile. The service 111 // exists in incognito mode; scores will be initialised using the score from 112 // the profile that the incognito session was created from, and will increase 113 // and decrease as usual. Engagement earned or decayed in incognito will not 114 // be persisted or reflected in the original profile. 115 // 116 // This method must be called on the UI thread. 117 static SiteEngagementService* Get(Profile* profile); 118 119 // Returns the maximum possible amount of engagement that a site can accrue. 120 static double GetMaxPoints(); 121 122 // Returns whether or not the site engagement service is enabled. 123 static bool IsEnabled(); 124 125 // Returns the score for |origin| based on |settings|. Can be called on any 126 // thread and does not cause any cleanup, decay, etc. 127 // 128 // Should only be used if you cannot create a SiteEngagementService (i.e. you 129 // cannot run on the UI thread). 130 static double GetScoreFromSettings(HostContentSettingsMap* settings, 131 const GURL& origin); 132 133 // Retrieves all details. Can be called from a background thread. |now| must 134 // be the current timestamp. Takes a scoped_refptr to keep 135 // HostContentSettingsMap alive. See crbug.com/901287. 136 static std::vector<mojom::SiteEngagementDetails> GetAllDetailsInBackground( 137 base::Time now, 138 scoped_refptr<HostContentSettingsMap> map); 139 140 explicit SiteEngagementService(Profile* profile); 141 ~SiteEngagementService() override; 142 143 // KeyedService support: 144 void Shutdown() override; 145 146 // Returns the engagement level of |url|. 147 blink::mojom::EngagementLevel GetEngagementLevel(const GURL& url) const; 148 149 // Returns an array of engagement score details for all origins which have 150 // a score, whether due to direct engagement, or other factors that cause 151 // an engagement bonus to be applied. 152 // 153 // Note that this method is quite expensive, so try to avoid calling it in 154 // performance-critical code. 155 std::vector<mojom::SiteEngagementDetails> GetAllDetails() const; 156 157 // Return an array of engagement score details for all origins which have 158 // had engagement since the specified time. 159 // 160 // Note that this method is quite expensive, so try to avoid calling it in 161 // performance-critical code. 162 std::vector<mojom::SiteEngagementDetails> GetAllDetailsEngagedInTimePeriod( 163 browsing_data::TimePeriod time_period) const; 164 165 // Update the engagement score of |url| for a notification interaction. 166 void HandleNotificationInteraction(const GURL& url); 167 168 // Returns whether the engagement service has enough data to make meaningful 169 // decisions. Clients should avoid using engagement in their heuristic until 170 // this is true. 171 bool IsBootstrapped() const; 172 173 // Returns whether |url| has at least the given |level| of engagement. 174 bool IsEngagementAtLeast(const GURL& url, 175 blink::mojom::EngagementLevel level) const; 176 177 // Resets the base engagement for |url| to |score|, clearing daily limits. Any 178 // bonus engagement that |url| has acquired is not affected by this method, so 179 // the result of GetScore(|url|) may not be the same as |score|. 180 void ResetBaseScoreForURL(const GURL& url, double score); 181 182 // Update the last time |url| was opened from an installed shortcut (hosted in 183 // |web_contents|) to be clock_->Now(). 184 void SetLastShortcutLaunchTime(content::WebContents* web_contents, 185 const GURL& url); 186 187 void HelperCreated(SiteEngagementService::Helper* helper); 188 void HelperDeleted(SiteEngagementService::Helper* helper); 189 190 // Returns the site engagement details for the specified |url|. 191 mojom::SiteEngagementDetails GetDetails(const GURL& url) const; 192 193 // Overridden from SiteEngagementScoreProvider. 194 double GetScore(const GURL& url) const override; 195 double GetTotalEngagementPoints() const override; 196 197 // Just forwards calls AddPoints. 198 void AddPointsForTesting(const GURL& url, double points); 199 200 private: 201 friend class SiteEngagementObserver; 202 friend class SiteEngagementServiceTest; 203 friend class web_app::WebAppEngagementBrowserTest; 204 FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, CheckHistograms); 205 FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, CleanupEngagementScores); 206 FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, 207 CleanupMovesScoreBackToNow); 208 FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, 209 CleanupMovesScoreBackToRebase); 210 FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, 211 CleanupEngagementScoresProportional); 212 FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, GetMedianEngagement); 213 FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, GetTotalNavigationPoints); 214 FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, GetTotalUserInputPoints); 215 FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, 216 GetTotalNotificationPoints); 217 FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, RestrictedToHTTPAndHTTPS); 218 FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, LastShortcutLaunch); 219 FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, 220 CleanupOriginsOnHistoryDeletion); 221 FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, IsBootstrapped); 222 FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, EngagementLevel); 223 FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, Observers); 224 FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, ScoreDecayHistograms); 225 FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, LastEngagementTime); 226 FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, 227 IncognitoEngagementService); 228 FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, GetScoreFromSettings); 229 FRIEND_TEST_ALL_PREFIXES(banners::AppBannerManagerBrowserTest, 230 WebAppBannerNeedsEngagement); 231 FRIEND_TEST_ALL_PREFIXES(AppBannerSettingsHelperTest, SiteEngagementTrigger); 232 FRIEND_TEST_ALL_PREFIXES(HostedAppPWAOnlyTest, EngagementHistogram); 233 234 #if defined(OS_ANDROID) 235 // Shim class to expose the service to Java. 236 friend class SiteEngagementServiceAndroid; 237 SiteEngagementServiceAndroid* GetAndroidService() const; 238 void SetAndroidService( 239 std::unique_ptr<SiteEngagementServiceAndroid> android_service); 240 #endif 241 242 // Only used in tests. 243 SiteEngagementService(Profile* profile, base::Clock* clock); 244 245 // Adds the specified number of points to the given origin, respecting the 246 // maximum limits for the day and overall. 247 void AddPoints(const GURL& url, double points); 248 249 // Retrieves the SiteEngagementScore object for |origin|. 250 SiteEngagementScore CreateEngagementScore(const GURL& origin) const; 251 252 // Runs site engagement maintenance tasks. 253 void AfterStartupTask(); 254 255 // Removes any origins which have decayed to 0 engagement. If 256 // |update_last_engagement_time| is true, the last engagement time of all 257 // origins is reset by calculating the delta between the last engagement event 258 // recorded by the site engagement service and the origin. The origin's last 259 // engagement time is then set to clock_->Now() - delta. 260 // 261 // If a user does not use the browser at all for some period of time, 262 // engagement is not decayed, and the state is restored equivalent to how they 263 // left it once they return. 264 void CleanupEngagementScores(bool update_last_engagement_time) const; 265 266 // Possibly records UMA metrics if we haven't recorded them lately. 267 void MaybeRecordMetrics(); 268 269 // Actually records metrics for the engagement in |details|. 270 void RecordMetrics(std::vector<mojom::SiteEngagementDetails>); 271 272 // Returns true if we should record engagement for this URL. Currently, 273 // engagement is only earned for HTTP and HTTPS. 274 bool ShouldRecordEngagement(const GURL& url) const; 275 276 // Get and set the last engagement time from prefs. 277 base::Time GetLastEngagementTime() const; 278 void SetLastEngagementTime(base::Time last_engagement_time) const; 279 280 // Get the maximum decay period and the stale period for last engagement 281 // times. 282 base::TimeDelta GetMaxDecayPeriod() const; 283 base::TimeDelta GetStalePeriod() const; 284 285 // Returns the median engagement score of all recorded origins. |details| must 286 // be sorted in ascending order of score. 287 double GetMedianEngagementFromSortedDetails( 288 const std::vector<mojom::SiteEngagementDetails>& details) const; 289 290 // Update the engagement score of the origin loaded in |web_contents| for 291 // media playing. The points awarded are discounted if the media is being 292 // played in a non-visible tab. 293 void HandleMediaPlaying(content::WebContents* web_contents, bool is_hidden); 294 295 // Update the engagement score of the origin loaded in |web_contents| for 296 // navigation. 297 void HandleNavigation(content::WebContents* web_contents, 298 ui::PageTransition transition); 299 300 // Update the engagement score of the origin loaded in |web_contents| for 301 // time-on-site, based on user input. 302 void HandleUserInput(content::WebContents* web_contents, EngagementType type); 303 304 // Called when the engagement for |url| loaded in |web_contents| is changed, 305 // due to an event of type |type|. Calls OnEngagementEvent in all observers. 306 // |web_contents| may be null if the engagement has increased when |url| is 307 // not in a tab, e.g. from a notification interaction. Also records 308 // engagement-type metrics. 309 void OnEngagementEvent(content::WebContents* web_contents, 310 const GURL& url, 311 EngagementType type); 312 313 // Returns true if the last engagement increasing event seen by the site 314 // engagement service was sufficiently long ago that we need to reset all 315 // scores to be relative to now. This ensures that users who do not use the 316 // browser for an extended period of time do not have their engagement decay. 317 bool IsLastEngagementStale() const; 318 319 // Overridden from history::HistoryServiceObserver: 320 void OnURLsDeleted(history::HistoryService* history_service, 321 const history::DeletionInfo& deletion_info) override; 322 323 // Returns the number of origins with maximum daily and total engagement 324 // respectively. 325 int OriginsWithMaxDailyEngagement() const; 326 327 // Update site engagement scores after a history deletion. 328 void UpdateEngagementScores( 329 const std::multiset<GURL>& deleted_url_origins, 330 bool expired, 331 const history::OriginCountAndLastVisitMap& remaining_origin_counts); 332 333 // Add and remove observers of this service. 334 void AddObserver(SiteEngagementObserver* observer); 335 void RemoveObserver(SiteEngagementObserver* observer); 336 337 Profile* profile_; 338 339 // The clock used to vend times. 340 base::Clock* clock_; 341 342 #if defined(OS_ANDROID) 343 std::unique_ptr<SiteEngagementServiceAndroid> android_service_; 344 #endif 345 346 // Metrics are recorded at non-incognito browser startup, and then 347 // approximately once per hour thereafter. Store the local time at which 348 // metrics were previously uploaded: the first event which affects any 349 // origin's engagement score after an hour has elapsed triggers the next 350 // upload. 351 base::Time last_metrics_time_; 352 353 // A list of observers. When any origin registers an engagement-increasing 354 // event, each observer's OnEngagementEvent method will be called. 355 base::ObserverList<SiteEngagementObserver>::Unchecked observer_list_; 356 357 base::WeakPtrFactory<SiteEngagementService> weak_factory_{this}; 358 359 DISALLOW_COPY_AND_ASSIGN(SiteEngagementService); 360 }; 361 362 #endif // CHROME_BROWSER_ENGAGEMENT_SITE_ENGAGEMENT_SERVICE_H_ 363