blob: c35b75de8c549a29f25ce06b983568324928d66d [file] [log] [blame]
// 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 CHROME_BROWSER_ENGAGEMENT_SITE_ENGAGEMENT_SERVICE_H_
#define CHROME_BROWSER_ENGAGEMENT_SITE_ENGAGEMENT_SERVICE_H_
#include <memory>
#include <set>
#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 "chrome/browser/engagement/site_engagement_details.mojom.h"
#include "components/history/core/browser/history_service_observer.h"
#include "components/keyed_service/core/keyed_service.h"
#include "third_party/WebKit/public/platform/site_engagement.mojom.h"
#include "ui/base/page_transition_types.h"
namespace base {
class Clock;
}
namespace banners {
FORWARD_DECLARE_TEST(AppBannerManagerBrowserTest,
ExperimentalFlowWebAppBannerNeedsEngagement);
}
namespace content {
class WebContents;
}
namespace history {
class HistoryService;
}
class GURL;
class HostContentSettingsMap;
class Profile;
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 history::HistoryServiceObserver,
public SiteEngagementScoreProvider {
public:
// 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
// ENGAGEMENT_LAST and added to SiteEngagementServiceEngagementType in
// tools/metrics/histograms/enums.xml.
// TODO(calamity): Document each of these engagement types.
enum EngagementType {
ENGAGEMENT_NAVIGATION,
ENGAGEMENT_KEYPRESS,
ENGAGEMENT_MOUSE,
ENGAGEMENT_TOUCH_GESTURE,
ENGAGEMENT_SCROLL,
ENGAGEMENT_MEDIA_HIDDEN,
ENGAGEMENT_MEDIA_VISIBLE,
ENGAGEMENT_WEBAPP_SHORTCUT_LAUNCH,
ENGAGEMENT_FIRST_DAILY_ENGAGEMENT,
ENGAGEMENT_NOTIFICATION_INTERACTION,
ENGAGEMENT_LAST,
};
// 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[];
// Returns the site engagement service attached to this profile. The service
// exists in incognito mode; scores will be initialised using the score from
// the profile 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 profile.
//
// This method must be called on the UI thread.
static SiteEngagementService* Get(Profile* profile);
// 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);
explicit SiteEngagementService(Profile* profile);
~SiteEngagementService() override;
// KeyedService support:
void Shutdown() 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.
std::vector<mojom::SiteEngagementDetails> GetAllDetails() 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);
void HelperCreated(SiteEngagementService::Helper* helper);
void HelperDeleted(SiteEngagementService::Helper* helper);
// 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);
private:
friend class SiteEngagementObserver;
friend class SiteEngagementServiceAndroid;
friend class SiteEngagementServiceTest;
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, GetMedianEngagement);
FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, GetTotalNavigationPoints);
FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, GetTotalUserInputPoints);
FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest,
GetTotalNotificationPoints);
FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, RestrictedToHTTPAndHTTPS);
FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, LastShortcutLaunch);
FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest,
CleanupOriginsOnHistoryDeletion);
FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, IsBootstrapped);
FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, EngagementLevel);
FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, Observers);
FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, ScoreDecayHistograms);
FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, LastEngagementTime);
FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest,
IncognitoEngagementService);
FRIEND_TEST_ALL_PREFIXES(SiteEngagementServiceTest, GetScoreFromSettings);
FRIEND_TEST_ALL_PREFIXES(banners::AppBannerManagerBrowserTest,
ExperimentalFlowWebAppBannerNeedsEngagement);
FRIEND_TEST_ALL_PREFIXES(AppBannerSettingsHelperTest, SiteEngagementTrigger);
FRIEND_TEST_ALL_PREFIXES(HostedAppPWAOnlyTest, EngagementHistogram);
#if defined(OS_ANDROID)
// Shim class to expose the service to Java.
SiteEngagementServiceAndroid* GetAndroidService() const;
void SetAndroidService(
std::unique_ptr<SiteEngagementServiceAndroid> android_service);
#endif
// Only used in tests.
SiteEngagementService(Profile* profile, std::unique_ptr<base::Clock> clock);
// 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);
// Retrieves the SiteEngagementScore object for |origin|.
SiteEngagementScore CreateEngagementScore(const GURL& origin) const;
// 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;
// Records UMA metrics.
void RecordMetrics();
// 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;
void SetLastEngagementTime(base::Time last_engagement_time) 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);
// Called if |url| changes to |level| engagement, and informs every Helper of
// the change.
void SendLevelChangeToHelpers(const GURL& url,
blink::mojom::EngagementLevel level);
// 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;
// Overridden from history::HistoryServiceObserver:
void OnURLsDeleted(history::HistoryService* history_service,
bool all_history,
bool expired,
const history::URLRows& deleted_rows,
const std::set<GURL>& favicon_urls) override;
// Returns the number of origins with maximum daily and total engagement
// respectively.
int OriginsWithMaxDailyEngagement() const;
int OriginsWithMaxEngagement(
const std::vector<mojom::SiteEngagementDetails>& details) const;
// Callback for the history service when it is asked for a map of origins to
// how many URLs corresponding to that origin remain in history.
void GetCountsAndLastVisitForOriginsComplete(
history::HistoryService* history_service,
const std::multiset<GURL>& deleted_url_origins,
bool expired,
const history::OriginCountAndLastVisitMap& remaining_origin_counts);
// Add and remove observers of this service.
void AddObserver(SiteEngagementObserver* observer);
void RemoveObserver(SiteEngagementObserver* observer);
Profile* profile_;
// The clock used to vend times.
std::unique_ptr<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_;
// All helpers currently attached to a WebContents.
std::set<SiteEngagementService::Helper*> helpers_;
// A list of observers. When any origin registers an engagement-increasing
// event, each observer's OnEngagementEvent method will be called.
base::ObserverList<SiteEngagementObserver> observer_list_;
base::WeakPtrFactory<SiteEngagementService> weak_factory_;
DISALLOW_COPY_AND_ASSIGN(SiteEngagementService);
};
#endif // CHROME_BROWSER_ENGAGEMENT_SITE_ENGAGEMENT_SERVICE_H_