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