1 // Copyright 2014 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.
5 #include "chrome/browser/banners/app_banner_settings_helper.h"
10 #include "base/command_line.h"
11 #include "base/metrics/field_trial.h"
12 #include "base/strings/string_number_conversions.h"
13 #include "base/strings/string_split.h"
14 #include "chrome/browser/banners/app_banner_data_fetcher.h"
15 #include "chrome/browser/banners/app_banner_metrics.h"
16 #include "chrome/browser/browser_process.h"
17 #include "chrome/browser/profiles/profile.h"
18 #include "chrome/common/chrome_switches.h"
19 #include "components/content_settings/core/browser/host_content_settings_map.h"
20 #include "components/content_settings/core/common/content_settings_pattern.h"
21 #include "components/rappor/rappor_utils.h"
22 #include "content/public/browser/web_contents.h"
23 #include "net/base/escape.h"
28 // Max number of apps (including ServiceWorker based web apps) that a particular
29 // site may show a banner for.
30 const size_t kMaxAppsPerSite
= 3;
32 // Oldest could show banner event we care about, in days.
33 const unsigned int kOldestCouldShowBannerEventInDays
= 14;
35 // Number of days that showing the banner will prevent it being seen again for.
36 const unsigned int kMinimumDaysBetweenBannerShows
= 60;
38 const unsigned int kNumberOfMinutesInADay
= 1440;
40 // Number of minutes between visits that will trigger a could show banner event.
41 // Defaults to the number of minutes in a day.
42 unsigned int gMinimumMinutesBetweenVisits
= kNumberOfMinutesInADay
;
44 // Number of days that the banner being blocked will prevent it being seen again
46 const unsigned int kMinimumBannerBlockedToBannerShown
= 90;
48 // Dictionary keys to use for the events.
49 const char* kBannerEventKeys
[] = {
50 "couldShowBannerEvents",
52 "didBlockBannerEvent",
53 "didAddToHomescreenEvent",
56 // Keys to use when storing BannerEvent structs.
57 const char kBannerTimeKey
[] = "time";
58 const char kBannerEngagementKey
[] = "engagement";
60 // Total site engagements where a banner could have been shown before
61 // a banner will actually be triggered.
62 double gTotalEngagementToTrigger
= 2;
64 // Engagement weight assigned to direct and indirect navigations.
65 // By default, a direct navigation is a page visit via ui::PAGE_TRANSITION_TYPED
66 // or ui::PAGE_TRANSITION_GENERATED.
67 double kDirectNavigationEngagement
= 1;
68 double kIndirectNavigationEnagagement
= 1;
70 scoped_ptr
<base::DictionaryValue
> GetOriginDict(
71 HostContentSettingsMap
* settings
,
72 const GURL
& origin_url
) {
74 return scoped_ptr
<base::DictionaryValue
>();
76 scoped_ptr
<base::Value
> value
= settings
->GetWebsiteSetting(
77 origin_url
, origin_url
, CONTENT_SETTINGS_TYPE_APP_BANNER
, std::string(),
80 return make_scoped_ptr(new base::DictionaryValue());
82 if (!value
->IsType(base::Value::TYPE_DICTIONARY
))
83 return make_scoped_ptr(new base::DictionaryValue());
85 return make_scoped_ptr(static_cast<base::DictionaryValue
*>(value
.release()));
88 base::DictionaryValue
* GetAppDict(base::DictionaryValue
* origin_dict
,
89 const std::string
& key_name
) {
90 base::DictionaryValue
* app_dict
= nullptr;
91 if (!origin_dict
->GetDictionaryWithoutPathExpansion(key_name
, &app_dict
)) {
92 // Don't allow more than kMaxAppsPerSite dictionaries.
93 if (origin_dict
->size() < kMaxAppsPerSite
) {
94 app_dict
= new base::DictionaryValue();
95 origin_dict
->SetWithoutPathExpansion(key_name
, make_scoped_ptr(app_dict
));
102 double GetEventEngagement(ui::PageTransition transition_type
) {
103 if (ui::PageTransitionCoreTypeIs(transition_type
,
104 ui::PAGE_TRANSITION_TYPED
) ||
105 ui::PageTransitionCoreTypeIs(transition_type
,
106 ui::PAGE_TRANSITION_GENERATED
)) {
107 return kDirectNavigationEngagement
;
109 return kIndirectNavigationEnagagement
;
113 // Queries a field trial for updates to the default engagement values assigned
114 // to direct and indirect navigations.
115 void UpdateEngagementWeights() {
116 // Expect a field trial value of "X:Y:Z", where X is the direct engagement
117 // value, Y is the indirect engagement value, and Z is the total required
118 // engagement to trigger the banner.
119 std::string weights
=
120 base::FieldTrialList::FindFullName("AppBannersEngagementWeights");
124 std::vector
<std::string
> tokens
= base::SplitString(
125 weights
, ":", base::TRIM_WHITESPACE
, base::SPLIT_WANT_NONEMPTY
);
126 if (tokens
.size() == 3) {
127 double direct_engagement
= -1;
128 double indirect_engagement
= -1;
129 double total_engagement
= -1;
131 // Ensure that we get valid doubles from the field trial, and that both
132 // values are greater than or equal to zero and less than or equal to the
133 // total engagement required to trigger the banner.
134 if (base::StringToDouble(tokens
[0], &direct_engagement
) &&
135 base::StringToDouble(tokens
[1], &indirect_engagement
) &&
136 base::StringToDouble(tokens
[2], &total_engagement
) &&
137 direct_engagement
>= 0 && indirect_engagement
>= 0 &&
138 total_engagement
> 0 && direct_engagement
<= total_engagement
&&
139 indirect_engagement
<= total_engagement
) {
140 AppBannerSettingsHelper::SetEngagementWeights(direct_engagement
,
141 indirect_engagement
);
142 AppBannerSettingsHelper::SetTotalEngagementToTrigger(total_engagement
);
147 // Queries a field trial for updates to the default number of minutes between
148 // site visits counted for the purposes of displaying a banner.
149 void UpdateMinutesBetweenVisits() {
150 std::string minutes_between_visits
=
151 base::FieldTrialList::FindFullName("AppBannersMinutesBetweenVisits");
152 if (minutes_between_visits
.empty())
155 int minimum_minutes
= 0;
156 if (base::StringToInt(minutes_between_visits
, &minimum_minutes
))
157 AppBannerSettingsHelper::SetMinimumMinutesBetweenVisits(minimum_minutes
);
162 void AppBannerSettingsHelper::ClearHistoryForURLs(
164 const std::set
<GURL
>& origin_urls
) {
165 HostContentSettingsMap
* settings
= profile
->GetHostContentSettingsMap();
166 for (const GURL
& origin_url
: origin_urls
) {
167 ContentSettingsPattern
pattern(ContentSettingsPattern::FromURL(origin_url
));
168 if (!pattern
.IsValid())
171 settings
->SetWebsiteSetting(pattern
, ContentSettingsPattern::Wildcard(),
172 CONTENT_SETTINGS_TYPE_APP_BANNER
, std::string(),
174 settings
->FlushLossyWebsiteSettings();
178 void AppBannerSettingsHelper::RecordBannerInstallEvent(
179 content::WebContents
* web_contents
,
180 const std::string
& package_name_or_start_url
,
181 AppBannerRapporMetric rappor_metric
) {
182 banners::TrackInstallEvent(banners::INSTALL_EVENT_WEB_APP_INSTALLED
);
184 AppBannerSettingsHelper::RecordBannerEvent(
185 web_contents
, web_contents
->GetURL(),
186 package_name_or_start_url
,
187 AppBannerSettingsHelper::APP_BANNER_EVENT_DID_ADD_TO_HOMESCREEN
,
188 banners::AppBannerDataFetcher::GetCurrentTime());
190 rappor::SampleDomainAndRegistryFromGURL(
191 g_browser_process
->rappor_service(),
192 (rappor_metric
== WEB
? "AppBanner.WebApp.Installed"
193 : "AppBanner.NativeApp.Installed"),
194 web_contents
->GetURL());
197 void AppBannerSettingsHelper::RecordBannerDismissEvent(
198 content::WebContents
* web_contents
,
199 const std::string
& package_name_or_start_url
,
200 AppBannerRapporMetric rappor_metric
) {
201 banners::TrackDismissEvent(banners::DISMISS_EVENT_CLOSE_BUTTON
);
203 AppBannerSettingsHelper::RecordBannerEvent(
204 web_contents
, web_contents
->GetURL(),
205 package_name_or_start_url
,
206 AppBannerSettingsHelper::APP_BANNER_EVENT_DID_BLOCK
,
207 banners::AppBannerDataFetcher::GetCurrentTime());
209 rappor::SampleDomainAndRegistryFromGURL(
210 g_browser_process
->rappor_service(),
211 (rappor_metric
== WEB
? "AppBanner.WebApp.Dismissed"
212 : "AppBanner.NativeApp.Dismissed"),
213 web_contents
->GetURL());
216 void AppBannerSettingsHelper::RecordBannerEvent(
217 content::WebContents
* web_contents
,
218 const GURL
& origin_url
,
219 const std::string
& package_name_or_start_url
,
220 AppBannerEvent event
,
222 DCHECK(event
!= APP_BANNER_EVENT_COULD_SHOW
);
225 Profile::FromBrowserContext(web_contents
->GetBrowserContext());
226 if (profile
->IsOffTheRecord() || package_name_or_start_url
.empty())
229 ContentSettingsPattern
pattern(ContentSettingsPattern::FromURL(origin_url
));
230 if (!pattern
.IsValid())
233 HostContentSettingsMap
* settings
= profile
->GetHostContentSettingsMap();
234 scoped_ptr
<base::DictionaryValue
> origin_dict
=
235 GetOriginDict(settings
, origin_url
);
239 base::DictionaryValue
* app_dict
=
240 GetAppDict(origin_dict
.get(), package_name_or_start_url
);
244 // Dates are stored in their raw form (i.e. not local dates) to be resilient
245 // to time zone changes.
246 std::string
event_key(kBannerEventKeys
[event
]);
247 app_dict
->SetDouble(event_key
, time
.ToInternalValue());
249 settings
->SetWebsiteSetting(pattern
, ContentSettingsPattern::Wildcard(),
250 CONTENT_SETTINGS_TYPE_APP_BANNER
, std::string(),
251 origin_dict
.release());
253 // App banner content settings are lossy, meaning they will not cause the
254 // prefs to become dirty. This is fine for most events, as if they are lost it
255 // just means the user will have to engage a little bit more. However the
256 // DID_ADD_TO_HOMESCREEN event should always be recorded to prevent
258 if (event
== APP_BANNER_EVENT_DID_ADD_TO_HOMESCREEN
)
259 settings
->FlushLossyWebsiteSettings();
262 void AppBannerSettingsHelper::RecordBannerCouldShowEvent(
263 content::WebContents
* web_contents
,
264 const GURL
& origin_url
,
265 const std::string
& package_name_or_start_url
,
267 ui::PageTransition transition_type
) {
269 Profile::FromBrowserContext(web_contents
->GetBrowserContext());
270 if (profile
->IsOffTheRecord() || package_name_or_start_url
.empty())
273 ContentSettingsPattern
pattern(ContentSettingsPattern::FromURL(origin_url
));
274 if (!pattern
.IsValid())
277 HostContentSettingsMap
* settings
= profile
->GetHostContentSettingsMap();
278 scoped_ptr
<base::DictionaryValue
> origin_dict
=
279 GetOriginDict(settings
, origin_url
);
283 base::DictionaryValue
* app_dict
=
284 GetAppDict(origin_dict
.get(), package_name_or_start_url
);
288 std::string
event_key(kBannerEventKeys
[APP_BANNER_EVENT_COULD_SHOW
]);
289 double engagement
= GetEventEngagement(transition_type
);
291 base::ListValue
* could_show_list
= nullptr;
292 if (!app_dict
->GetList(event_key
, &could_show_list
)) {
293 could_show_list
= new base::ListValue();
294 app_dict
->Set(event_key
, make_scoped_ptr(could_show_list
));
297 // Trim any items that are older than we should care about. For comparisons
298 // the times are converted to local dates.
299 base::Time date
= BucketTimeToResolution(time
, gMinimumMinutesBetweenVisits
);
300 base::ValueVector::iterator it
= could_show_list
->begin();
301 while (it
!= could_show_list
->end()) {
302 if ((*it
)->IsType(base::Value::TYPE_DICTIONARY
)) {
303 base::DictionaryValue
* internal_value
;
304 double internal_date
;
305 (*it
)->GetAsDictionary(&internal_value
);
307 if (internal_value
->GetDouble(kBannerTimeKey
, &internal_date
)) {
308 base::Time other_date
=
309 BucketTimeToResolution(base::Time::FromInternalValue(internal_date
),
310 gMinimumMinutesBetweenVisits
);
311 if (other_date
== date
) {
312 double other_engagement
= 0;
313 if (internal_value
->GetDouble(kBannerEngagementKey
,
314 &other_engagement
) &&
315 other_engagement
>= engagement
) {
316 // This date has already been added, but with an equal or higher
317 // engagement. Don't add the date again. If the conditional fails,
318 // fall to the end of the loop where the existing entry is deleted.
322 base::TimeDelta delta
= date
- other_date
;
324 base::TimeDelta::FromDays(kOldestCouldShowBannerEventInDays
)) {
332 // Either this date is older than we care about, or it isn't in the correct
333 // format, or it is the same as the current date but with a lower
334 // engagement, so remove it.
335 it
= could_show_list
->Erase(it
, nullptr);
338 // Dates are stored in their raw form (i.e. not local dates) to be resilient
339 // to time zone changes.
340 scoped_ptr
<base::DictionaryValue
> value(new base::DictionaryValue());
341 value
->SetDouble(kBannerTimeKey
, time
.ToInternalValue());
342 value
->SetDouble(kBannerEngagementKey
, engagement
);
343 could_show_list
->Append(value
.Pass());
345 settings
->SetWebsiteSetting(pattern
, ContentSettingsPattern::Wildcard(),
346 CONTENT_SETTINGS_TYPE_APP_BANNER
, std::string(),
347 origin_dict
.release());
350 bool AppBannerSettingsHelper::ShouldShowBanner(
351 content::WebContents
* web_contents
,
352 const GURL
& origin_url
,
353 const std::string
& package_name_or_start_url
,
355 // Ignore all checks if the flag to do so is set.
356 if (base::CommandLine::ForCurrentProcess()->HasSwitch(
357 switches::kBypassAppBannerEngagementChecks
)) {
361 // Don't show if it has been added to the homescreen.
362 base::Time added_time
=
363 GetSingleBannerEvent(web_contents
, origin_url
, package_name_or_start_url
,
364 APP_BANNER_EVENT_DID_ADD_TO_HOMESCREEN
);
365 if (!added_time
.is_null()) {
366 banners::TrackDisplayEvent(banners::DISPLAY_EVENT_INSTALLED_PREVIOUSLY
);
370 base::Time blocked_time
=
371 GetSingleBannerEvent(web_contents
, origin_url
, package_name_or_start_url
,
372 APP_BANNER_EVENT_DID_BLOCK
);
374 // Null times are in the distant past, so the delta between real times and
375 // null events will always be greater than the limits.
376 if (time
- blocked_time
<
377 base::TimeDelta::FromDays(kMinimumBannerBlockedToBannerShown
)) {
378 banners::TrackDisplayEvent(banners::DISPLAY_EVENT_BLOCKED_PREVIOUSLY
);
382 base::Time shown_time
=
383 GetSingleBannerEvent(web_contents
, origin_url
, package_name_or_start_url
,
384 APP_BANNER_EVENT_DID_SHOW
);
385 if (time
- shown_time
<
386 base::TimeDelta::FromDays(kMinimumDaysBetweenBannerShows
)) {
387 banners::TrackDisplayEvent(banners::DISPLAY_EVENT_IGNORED_PREVIOUSLY
);
391 std::vector
<BannerEvent
> could_show_events
= GetCouldShowBannerEvents(
392 web_contents
, origin_url
, package_name_or_start_url
);
394 // Return true if the total engagement of each applicable could show event
395 // meets the trigger threshold.
396 double total_engagement
= 0;
397 for (const auto& event
: could_show_events
)
398 total_engagement
+= event
.engagement
;
400 if (total_engagement
< gTotalEngagementToTrigger
) {
401 banners::TrackDisplayEvent(banners::DISPLAY_EVENT_NOT_VISITED_ENOUGH
);
408 std::vector
<AppBannerSettingsHelper::BannerEvent
>
409 AppBannerSettingsHelper::GetCouldShowBannerEvents(
410 content::WebContents
* web_contents
,
411 const GURL
& origin_url
,
412 const std::string
& package_name_or_start_url
) {
413 std::vector
<BannerEvent
> result
;
414 if (package_name_or_start_url
.empty())
418 Profile::FromBrowserContext(web_contents
->GetBrowserContext());
419 HostContentSettingsMap
* settings
= profile
->GetHostContentSettingsMap();
420 scoped_ptr
<base::DictionaryValue
> origin_dict
=
421 GetOriginDict(settings
, origin_url
);
426 base::DictionaryValue
* app_dict
=
427 GetAppDict(origin_dict
.get(), package_name_or_start_url
);
431 std::string
event_key(kBannerEventKeys
[APP_BANNER_EVENT_COULD_SHOW
]);
432 base::ListValue
* could_show_list
= nullptr;
433 if (!app_dict
->GetList(event_key
, &could_show_list
))
436 for (auto value
: *could_show_list
) {
437 if (value
->IsType(base::Value::TYPE_DICTIONARY
)) {
438 base::DictionaryValue
* internal_value
;
439 double internal_date
= 0;
440 value
->GetAsDictionary(&internal_value
);
441 double engagement
= 0;
443 if (internal_value
->GetDouble(kBannerTimeKey
, &internal_date
) &&
444 internal_value
->GetDouble(kBannerEngagementKey
, &engagement
)) {
445 base::Time date
= base::Time::FromInternalValue(internal_date
);
446 result
.push_back({date
, engagement
});
454 base::Time
AppBannerSettingsHelper::GetSingleBannerEvent(
455 content::WebContents
* web_contents
,
456 const GURL
& origin_url
,
457 const std::string
& package_name_or_start_url
,
458 AppBannerEvent event
) {
459 DCHECK(event
!= APP_BANNER_EVENT_COULD_SHOW
);
460 DCHECK(event
< APP_BANNER_EVENT_NUM_EVENTS
);
462 if (package_name_or_start_url
.empty())
466 Profile::FromBrowserContext(web_contents
->GetBrowserContext());
467 HostContentSettingsMap
* settings
= profile
->GetHostContentSettingsMap();
468 scoped_ptr
<base::DictionaryValue
> origin_dict
=
469 GetOriginDict(settings
, origin_url
);
474 base::DictionaryValue
* app_dict
=
475 GetAppDict(origin_dict
.get(), package_name_or_start_url
);
479 std::string
event_key(kBannerEventKeys
[event
]);
480 double internal_time
;
481 if (!app_dict
->GetDouble(event_key
, &internal_time
))
484 return base::Time::FromInternalValue(internal_time
);
487 void AppBannerSettingsHelper::SetEngagementWeights(double direct_engagement
,
488 double indirect_engagement
) {
489 kDirectNavigationEngagement
= direct_engagement
;
490 kIndirectNavigationEnagagement
= indirect_engagement
;
493 void AppBannerSettingsHelper::SetMinimumMinutesBetweenVisits(
494 unsigned int minutes
) {
495 gMinimumMinutesBetweenVisits
= minutes
;
498 void AppBannerSettingsHelper::SetTotalEngagementToTrigger(
499 double total_engagement
) {
500 gTotalEngagementToTrigger
= total_engagement
;
503 // Given a time, returns that time scoped to the nearest minute resolution
504 // locally. For example, if the resolution is one hour, this function will
505 // return the time to the closest (previous) hour in the local time zone.
506 base::Time
AppBannerSettingsHelper::BucketTimeToResolution(
508 unsigned int minutes
) {
509 // Only support resolutions smaller than or equal to one day. Enforce
510 // that resolutions divide evenly into one day. Otherwise, default to a
511 // day resolution (each time converted to midnight local time).
512 if (minutes
== 0 || minutes
>= kNumberOfMinutesInADay
||
513 kNumberOfMinutesInADay
% minutes
!= 0) {
514 return time
.LocalMidnight();
517 // Extract the number of minutes past midnight in local time. Divide that
518 // number by the resolution size, and return the time converted to local
519 // midnight with the resulting truncated number added.
520 base::Time::Exploded exploded
;
521 time
.LocalExplode(&exploded
);
522 int total_minutes
= exploded
.hour
* 60 + exploded
.minute
;
524 // Use truncating integer division here.
525 return time
.LocalMidnight() +
526 base::TimeDelta::FromMinutes((total_minutes
/ minutes
) * minutes
);
529 void AppBannerSettingsHelper::UpdateFromFieldTrial() {
530 UpdateEngagementWeights();
531 UpdateMinutesBetweenVisits();