blob: 068a7c509b0ee3e750ee93e80b8581ec54117aed [file] [log] [blame]
// Copyright 2014 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 "chrome/browser/banners/app_banner_settings_helper.h"
#include <stddef.h>
#include <algorithm>
#include <string>
#include <utility>
#include "base/command_line.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/field_trial.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "chrome/browser/banners/app_banner_data_fetcher.h"
#include "chrome/browser/banners/app_banner_metrics.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/engagement/site_engagement_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_switches.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/content_settings_pattern.h"
#include "components/rappor/rappor_utils.h"
#include "components/variations/variations_associated_data.h"
#include "content/public/browser/web_contents.h"
#include "net/base/escape.h"
#include "url/gurl.h"
namespace {
// Max number of apps (including ServiceWorker based web apps) that a particular
// site may show a banner for.
const size_t kMaxAppsPerSite = 3;
// Oldest could show banner event we care about, in days.
const unsigned int kOldestCouldShowBannerEventInDays = 14;
// Number of days that showing the banner will prevent it being seen again for.
const unsigned int kMinimumDaysBetweenBannerShows = 60;
const unsigned int kNumberOfMinutesInADay = 1440;
// Number of days that the banner being blocked will prevent it being seen again
// for.
const unsigned int kMinimumBannerBlockedToBannerShown = 90;
// Default scores assigned to direct and indirect navigations respectively.
const unsigned int kDefaultDirectNavigationEngagement = 1;
const unsigned int kDefaultIndirectNavigationEngagement = 1;
// Default number of navigations required to trigger the banner.
const unsigned int kDefaultTotalEngagementToTrigger = 2;
// Dictionary keys to use for the events.
const char* kBannerEventKeys[] = {
"couldShowBannerEvents",
"didShowBannerEvent",
"didBlockBannerEvent",
"didAddToHomescreenEvent",
};
// Keys to use when storing BannerEvent structs.
const char kBannerTimeKey[] = "time";
const char kBannerEngagementKey[] = "engagement";
// Keys to use when querying the variations params.
const char kBannerParamsKey[] = "AppBannerTriggering";
const char kBannerParamsDirectKey[] = "direct";
const char kBannerParamsIndirectKey[] = "indirect";
const char kBannerParamsTotalKey[] = "total";
const char kBannerParamsMinutesKey[] = "minutes";
const char kBannerSiteEngagementParamsKey[] = "app_banner_triggering";
const char kBannerSiteEngagementParamsTotalKey[] =
"app_banner_triggering_total";
// Engagement weight assigned to direct and indirect navigations.
// By default, a direct navigation is a page visit via ui::PAGE_TRANSITION_TYPED
// or ui::PAGE_TRANSITION_GENERATED.
double gDirectNavigationEngagement = kDefaultDirectNavigationEngagement;
double gIndirectNavigationEnagagement = kDefaultIndirectNavigationEngagement;
// Number of minutes between visits that will trigger a could show banner event.
// Defaults to the number of minutes in a day.
unsigned int gMinimumMinutesBetweenVisits = kNumberOfMinutesInADay;
// Total engagement score required before a banner will actually be triggered.
double gTotalEngagementToTrigger = kDefaultTotalEngagementToTrigger;
std::unique_ptr<base::DictionaryValue> GetOriginDict(
HostContentSettingsMap* settings,
const GURL& origin_url) {
if (!settings)
return base::WrapUnique(new base::DictionaryValue());
std::unique_ptr<base::DictionaryValue> dict =
base::DictionaryValue::From(settings->GetWebsiteSetting(
origin_url, origin_url, CONTENT_SETTINGS_TYPE_APP_BANNER,
std::string(), NULL));
if (!dict)
return base::WrapUnique(new base::DictionaryValue());
return dict;
}
base::DictionaryValue* GetAppDict(base::DictionaryValue* origin_dict,
const std::string& key_name) {
base::DictionaryValue* app_dict = nullptr;
if (!origin_dict->GetDictionaryWithoutPathExpansion(key_name, &app_dict)) {
// Don't allow more than kMaxAppsPerSite dictionaries.
if (origin_dict->size() < kMaxAppsPerSite) {
app_dict = new base::DictionaryValue();
origin_dict->SetWithoutPathExpansion(key_name,
base::WrapUnique(app_dict));
}
}
return app_dict;
}
double GetEventEngagement(ui::PageTransition transition_type) {
if (ui::PageTransitionCoreTypeIs(transition_type,
ui::PAGE_TRANSITION_TYPED) ||
ui::PageTransitionCoreTypeIs(transition_type,
ui::PAGE_TRANSITION_GENERATED)) {
return gDirectNavigationEngagement;
} else {
return gIndirectNavigationEnagagement;
}
}
// Queries variations for the maximum site engagement score required to trigger
// the banner showing.
void UpdateSiteEngagementToTrigger() {
std::string total_param = variations::GetVariationParamValue(
SiteEngagementService::kEngagementParams,
kBannerSiteEngagementParamsTotalKey);
if (!total_param.empty()) {
double total_engagement = -1;
if (base::StringToDouble(total_param, &total_engagement) &&
total_engagement > 0) {
AppBannerSettingsHelper::SetTotalEngagementToTrigger(total_engagement);
}
}
}
// Queries variations for updates to the default engagement values assigned
// to direct and indirect navigations.
void UpdateEngagementWeights() {
std::string direct_param = variations::GetVariationParamValue(
kBannerParamsKey, kBannerParamsDirectKey);
std::string indirect_param = variations::GetVariationParamValue(
kBannerParamsKey, kBannerParamsIndirectKey);
std::string total_param = variations::GetVariationParamValue(
kBannerParamsKey, kBannerParamsTotalKey);
if (!direct_param.empty() && !indirect_param.empty() &&
!total_param.empty()) {
double direct_engagement = -1;
double indirect_engagement = -1;
double total_engagement = -1;
// Ensure that we get valid doubles from the field trial, and that both
// values are greater than or equal to zero and less than or equal to the
// total engagement required to trigger the banner.
if (base::StringToDouble(direct_param, &direct_engagement) &&
base::StringToDouble(indirect_param, &indirect_engagement) &&
base::StringToDouble(total_param, &total_engagement) &&
direct_engagement >= 0 && indirect_engagement >= 0 &&
total_engagement > 0 && direct_engagement <= total_engagement &&
indirect_engagement <= total_engagement) {
AppBannerSettingsHelper::SetEngagementWeights(direct_engagement,
indirect_engagement);
AppBannerSettingsHelper::SetTotalEngagementToTrigger(total_engagement);
}
}
}
// Queries variation for updates to the default number of minutes between
// site visits counted for the purposes of displaying a banner.
void UpdateMinutesBetweenVisits() {
std::string param = variations::GetVariationParamValue(
kBannerParamsKey, kBannerParamsMinutesKey);
if (!param.empty()) {
int minimum_minutes = 0;
if (base::StringToInt(param, &minimum_minutes))
AppBannerSettingsHelper::SetMinimumMinutesBetweenVisits(minimum_minutes);
}
}
// Returns the site engagement karma score for the given origin URL under the
// current profile.
double GetSiteEngagementScoreForOrigin(
content::WebContents* web_contents,
const GURL& origin_url) {
SiteEngagementService* service = SiteEngagementService::Get(
Profile::FromBrowserContext(web_contents->GetBrowserContext()));
return service ? service->GetScore(origin_url) : 0;
}
} // namespace
void AppBannerSettingsHelper::ClearHistoryForURLs(
Profile* profile,
const std::set<GURL>& origin_urls) {
HostContentSettingsMap* settings =
HostContentSettingsMapFactory::GetForProfile(profile);
for (const GURL& origin_url : origin_urls) {
settings->SetWebsiteSettingDefaultScope(origin_url, GURL(),
CONTENT_SETTINGS_TYPE_APP_BANNER,
std::string(), nullptr);
settings->FlushLossyWebsiteSettings();
}
}
void AppBannerSettingsHelper::RecordBannerInstallEvent(
content::WebContents* web_contents,
const std::string& package_name_or_start_url,
AppBannerRapporMetric rappor_metric) {
banners::TrackInstallEvent(banners::INSTALL_EVENT_WEB_APP_INSTALLED);
AppBannerSettingsHelper::RecordBannerEvent(
web_contents, web_contents->GetURL(),
package_name_or_start_url,
AppBannerSettingsHelper::APP_BANNER_EVENT_DID_ADD_TO_HOMESCREEN,
banners::AppBannerDataFetcher::GetCurrentTime());
rappor::SampleDomainAndRegistryFromGURL(
g_browser_process->rappor_service(),
(rappor_metric == WEB ? "AppBanner.WebApp.Installed"
: "AppBanner.NativeApp.Installed"),
web_contents->GetURL());
}
void AppBannerSettingsHelper::RecordBannerDismissEvent(
content::WebContents* web_contents,
const std::string& package_name_or_start_url,
AppBannerRapporMetric rappor_metric) {
banners::TrackDismissEvent(banners::DISMISS_EVENT_CLOSE_BUTTON);
AppBannerSettingsHelper::RecordBannerEvent(
web_contents, web_contents->GetURL(),
package_name_or_start_url,
AppBannerSettingsHelper::APP_BANNER_EVENT_DID_BLOCK,
banners::AppBannerDataFetcher::GetCurrentTime());
rappor::SampleDomainAndRegistryFromGURL(
g_browser_process->rappor_service(),
(rappor_metric == WEB ? "AppBanner.WebApp.Dismissed"
: "AppBanner.NativeApp.Dismissed"),
web_contents->GetURL());
}
void AppBannerSettingsHelper::RecordBannerEvent(
content::WebContents* web_contents,
const GURL& origin_url,
const std::string& package_name_or_start_url,
AppBannerEvent event,
base::Time time) {
DCHECK(event != APP_BANNER_EVENT_COULD_SHOW);
Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
if (profile->IsOffTheRecord() || package_name_or_start_url.empty())
return;
HostContentSettingsMap* settings =
HostContentSettingsMapFactory::GetForProfile(profile);
std::unique_ptr<base::DictionaryValue> origin_dict =
GetOriginDict(settings, origin_url);
if (!origin_dict)
return;
base::DictionaryValue* app_dict =
GetAppDict(origin_dict.get(), package_name_or_start_url);
if (!app_dict)
return;
// Dates are stored in their raw form (i.e. not local dates) to be resilient
// to time zone changes.
std::string event_key(kBannerEventKeys[event]);
app_dict->SetDouble(event_key, time.ToInternalValue());
settings->SetWebsiteSettingDefaultScope(origin_url, GURL(),
CONTENT_SETTINGS_TYPE_APP_BANNER,
std::string(), origin_dict.release());
// App banner content settings are lossy, meaning they will not cause the
// prefs to become dirty. This is fine for most events, as if they are lost it
// just means the user will have to engage a little bit more. However the
// DID_ADD_TO_HOMESCREEN event should always be recorded to prevent
// spamminess.
if (event == APP_BANNER_EVENT_DID_ADD_TO_HOMESCREEN)
settings->FlushLossyWebsiteSettings();
}
void AppBannerSettingsHelper::RecordBannerCouldShowEvent(
content::WebContents* web_contents,
const GURL& origin_url,
const std::string& package_name_or_start_url,
base::Time time,
ui::PageTransition transition_type) {
Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
if (profile->IsOffTheRecord() || package_name_or_start_url.empty())
return;
HostContentSettingsMap* settings =
HostContentSettingsMapFactory::GetForProfile(profile);
std::unique_ptr<base::DictionaryValue> origin_dict =
GetOriginDict(settings, origin_url);
if (!origin_dict)
return;
base::DictionaryValue* app_dict =
GetAppDict(origin_dict.get(), package_name_or_start_url);
if (!app_dict)
return;
std::string event_key(kBannerEventKeys[APP_BANNER_EVENT_COULD_SHOW]);
double engagement = GetEventEngagement(transition_type);
base::ListValue* could_show_list = nullptr;
if (!app_dict->GetList(event_key, &could_show_list)) {
could_show_list = new base::ListValue();
app_dict->Set(event_key, base::WrapUnique(could_show_list));
}
// Trim any items that are older than we should care about. For comparisons
// the times are converted to local dates.
base::Time date = BucketTimeToResolution(time, gMinimumMinutesBetweenVisits);
for (auto it = could_show_list->begin(); it != could_show_list->end();) {
if ((*it)->IsType(base::Value::TYPE_DICTIONARY)) {
base::DictionaryValue* internal_value;
double internal_date;
(*it)->GetAsDictionary(&internal_value);
if (internal_value->GetDouble(kBannerTimeKey, &internal_date)) {
base::Time other_date =
BucketTimeToResolution(base::Time::FromInternalValue(internal_date),
gMinimumMinutesBetweenVisits);
if (other_date == date) {
double other_engagement = 0;
if (internal_value->GetDouble(kBannerEngagementKey,
&other_engagement) &&
other_engagement >= engagement) {
// This date has already been added, but with an equal or higher
// engagement. Don't add the date again. If the conditional fails,
// fall to the end of the loop where the existing entry is deleted.
return;
}
} else {
base::TimeDelta delta = date - other_date;
if (delta <
base::TimeDelta::FromDays(kOldestCouldShowBannerEventInDays)) {
++it;
continue;
}
}
}
}
// Either this date is older than we care about, or it isn't in the correct
// format, or it is the same as the current date but with a lower
// engagement, so remove it.
it = could_show_list->Erase(it, nullptr);
}
// Dates are stored in their raw form (i.e. not local dates) to be resilient
// to time zone changes.
std::unique_ptr<base::DictionaryValue> value(new base::DictionaryValue());
value->SetDouble(kBannerTimeKey, time.ToInternalValue());
value->SetDouble(kBannerEngagementKey, engagement);
could_show_list->Append(std::move(value));
settings->SetWebsiteSettingDefaultScope(origin_url, GURL(),
CONTENT_SETTINGS_TYPE_APP_BANNER,
std::string(), origin_dict.release());
}
bool AppBannerSettingsHelper::ShouldShowBanner(
content::WebContents* web_contents,
const GURL& origin_url,
const std::string& package_name_or_start_url,
base::Time time) {
// Ignore all checks if the flag to do so is set.
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kBypassAppBannerEngagementChecks)) {
return true;
}
// Never show a banner when the package name or URL is empty.
if (package_name_or_start_url.empty())
return false;
// Don't show if it has been added to the homescreen.
base::Time added_time =
GetSingleBannerEvent(web_contents, origin_url, package_name_or_start_url,
APP_BANNER_EVENT_DID_ADD_TO_HOMESCREEN);
if (!added_time.is_null()) {
banners::TrackDisplayEvent(banners::DISPLAY_EVENT_INSTALLED_PREVIOUSLY);
return false;
}
base::Time blocked_time =
GetSingleBannerEvent(web_contents, origin_url, package_name_or_start_url,
APP_BANNER_EVENT_DID_BLOCK);
// Null times are in the distant past, so the delta between real times and
// null events will always be greater than the limits.
if (time - blocked_time <
base::TimeDelta::FromDays(kMinimumBannerBlockedToBannerShown)) {
banners::TrackDisplayEvent(banners::DISPLAY_EVENT_BLOCKED_PREVIOUSLY);
return false;
}
base::Time shown_time =
GetSingleBannerEvent(web_contents, origin_url, package_name_or_start_url,
APP_BANNER_EVENT_DID_SHOW);
if (time - shown_time <
base::TimeDelta::FromDays(kMinimumDaysBetweenBannerShows)) {
banners::TrackDisplayEvent(banners::DISPLAY_EVENT_IGNORED_PREVIOUSLY);
return false;
}
double total_engagement = 0;
if (ShouldUseSiteEngagementScore()) {
total_engagement =
GetSiteEngagementScoreForOrigin(web_contents, origin_url);
} else {
std::vector<BannerEvent> could_show_events = GetCouldShowBannerEvents(
web_contents, origin_url, package_name_or_start_url);
for (const auto& event : could_show_events)
total_engagement += event.engagement;
}
if (total_engagement < gTotalEngagementToTrigger) {
banners::TrackDisplayEvent(banners::DISPLAY_EVENT_NOT_VISITED_ENOUGH);
return false;
}
return true;
}
std::vector<AppBannerSettingsHelper::BannerEvent>
AppBannerSettingsHelper::GetCouldShowBannerEvents(
content::WebContents* web_contents,
const GURL& origin_url,
const std::string& package_name_or_start_url) {
std::vector<BannerEvent> result;
Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
HostContentSettingsMap* settings =
HostContentSettingsMapFactory::GetForProfile(profile);
std::unique_ptr<base::DictionaryValue> origin_dict =
GetOriginDict(settings, origin_url);
if (!origin_dict)
return result;
base::DictionaryValue* app_dict =
GetAppDict(origin_dict.get(), package_name_or_start_url);
if (!app_dict)
return result;
std::string event_key(kBannerEventKeys[APP_BANNER_EVENT_COULD_SHOW]);
base::ListValue* could_show_list = nullptr;
if (!app_dict->GetList(event_key, &could_show_list))
return result;
for (const auto& value : *could_show_list) {
if (value->IsType(base::Value::TYPE_DICTIONARY)) {
base::DictionaryValue* internal_value;
double internal_date = 0;
value->GetAsDictionary(&internal_value);
double engagement = 0;
if (internal_value->GetDouble(kBannerTimeKey, &internal_date) &&
internal_value->GetDouble(kBannerEngagementKey, &engagement)) {
base::Time date = base::Time::FromInternalValue(internal_date);
result.push_back({date, engagement});
}
}
}
return result;
}
base::Time AppBannerSettingsHelper::GetSingleBannerEvent(
content::WebContents* web_contents,
const GURL& origin_url,
const std::string& package_name_or_start_url,
AppBannerEvent event) {
DCHECK(event != APP_BANNER_EVENT_COULD_SHOW);
DCHECK(event < APP_BANNER_EVENT_NUM_EVENTS);
Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
HostContentSettingsMap* settings =
HostContentSettingsMapFactory::GetForProfile(profile);
std::unique_ptr<base::DictionaryValue> origin_dict =
GetOriginDict(settings, origin_url);
if (!origin_dict)
return base::Time();
base::DictionaryValue* app_dict =
GetAppDict(origin_dict.get(), package_name_or_start_url);
if (!app_dict)
return base::Time();
std::string event_key(kBannerEventKeys[event]);
double internal_time;
if (!app_dict->GetDouble(event_key, &internal_time))
return base::Time();
return base::Time::FromInternalValue(internal_time);
}
void AppBannerSettingsHelper::RecordMinutesFromFirstVisitToShow(
content::WebContents* web_contents,
const GURL& origin_url,
const std::string& package_name_or_start_url,
base::Time time) {
std::vector<BannerEvent> could_show_events = GetCouldShowBannerEvents(
web_contents, origin_url, package_name_or_start_url);
int minutes = 0;
if (could_show_events.size())
minutes = (time - could_show_events[0].time).InMinutes();
banners::TrackMinutesFromFirstVisitToBannerShown(minutes);
}
void AppBannerSettingsHelper::SetEngagementWeights(double direct_engagement,
double indirect_engagement) {
gDirectNavigationEngagement = direct_engagement;
gIndirectNavigationEnagagement = indirect_engagement;
}
void AppBannerSettingsHelper::SetMinimumMinutesBetweenVisits(
unsigned int minutes) {
gMinimumMinutesBetweenVisits = minutes;
}
void AppBannerSettingsHelper::SetTotalEngagementToTrigger(
double total_engagement) {
gTotalEngagementToTrigger = total_engagement;
}
void AppBannerSettingsHelper::SetDefaultParameters() {
SetEngagementWeights(kDefaultDirectNavigationEngagement,
kDefaultIndirectNavigationEngagement);
SetMinimumMinutesBetweenVisits(kNumberOfMinutesInADay);
SetTotalEngagementToTrigger(kDefaultTotalEngagementToTrigger);
}
// Given a time, returns that time scoped to the nearest minute resolution
// locally. For example, if the resolution is one hour, this function will
// return the time to the closest (previous) hour in the local time zone.
base::Time AppBannerSettingsHelper::BucketTimeToResolution(
base::Time time,
unsigned int minutes) {
// Only support resolutions smaller than or equal to one day. Enforce
// that resolutions divide evenly into one day. Otherwise, default to a
// day resolution (each time converted to midnight local time).
if (minutes == 0 || minutes >= kNumberOfMinutesInADay ||
kNumberOfMinutesInADay % minutes != 0) {
return time.LocalMidnight();
}
// Extract the number of minutes past midnight in local time. Divide that
// number by the resolution size, and return the time converted to local
// midnight with the resulting truncated number added.
base::Time::Exploded exploded;
time.LocalExplode(&exploded);
int total_minutes = exploded.hour * 60 + exploded.minute;
// Use truncating integer division here.
return time.LocalMidnight() +
base::TimeDelta::FromMinutes((total_minutes / minutes) * minutes);
}
void AppBannerSettingsHelper::UpdateFromFieldTrial() {
// If we are using the site engagement score, only extract the total
// engagement to trigger from the params variations.
if (ShouldUseSiteEngagementScore()) {
UpdateSiteEngagementToTrigger();
} else {
UpdateEngagementWeights();
UpdateMinutesBetweenVisits();
}
}
bool AppBannerSettingsHelper::ShouldUseSiteEngagementScore() {
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kEnableSiteEngagementAppBanner)) {
return true;
}
// This experiment is controlled under the same key as the broader site
// engagement experiment rather than the banner experiment. This avoids cross
// pollution with other site engagement experiments. However, this experiment
// must only be active when there is one singular group under the banner
// experiment, otherwise the banner and site engagement banner experiments
// will conflict.
//
// Making the experiment active when a variations key is present allows us
// to have experiments which enable multiple features under site engagement.
std::string param = variations::GetVariationParamValue(
SiteEngagementService::kEngagementParams, kBannerSiteEngagementParamsKey);
return !param.empty();
}