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 "chrome/browser/banners/app_banner_data_fetcher.h"
12 #include "chrome/browser/banners/app_banner_metrics.h"
13 #include "chrome/browser/browser_process.h"
14 #include "chrome/browser/profiles/profile.h"
15 #include "chrome/common/chrome_switches.h"
16 #include "components/content_settings/core/browser/host_content_settings_map.h"
17 #include "components/content_settings/core/common/content_settings_pattern.h"
18 #include "components/rappor/rappor_utils.h"
19 #include "content/public/browser/web_contents.h"
20 #include "net/base/escape.h"
25 // Max number of apps (including ServiceWorker based web apps) that a particular
26 // site may show a banner for.
27 const size_t kMaxAppsPerSite
= 3;
29 // Oldest could show banner event we care about, in days.
30 const unsigned int kOldestCouldShowBannerEventInDays
= 14;
32 // Total site engagements where a banner could have been shown before
33 // a banner will actually be triggered.
34 const double kTotalEngagementToTrigger
= 2;
36 // Number of days that showing the banner will prevent it being seen again for.
37 const unsigned int kMinimumDaysBetweenBannerShows
= 60;
39 // Number of days that the banner being blocked will prevent it being seen again
41 const unsigned int kMinimumBannerBlockedToBannerShown
= 90;
43 // Dictionary keys to use for the events.
44 const char* kBannerEventKeys
[] = {
45 "couldShowBannerEvents",
47 "didBlockBannerEvent",
48 "didAddToHomescreenEvent",
51 // Keys to use when storing BannerEvent structs.
52 const char kBannerTimeKey
[] = "time";
53 const char kBannerEngagementKey
[] = "engagement";
55 // Engagement weight assigned to direct and indirect navigations.
56 // By default, a direct navigation is a page visit via ui::PAGE_TRANSITION_TYPED
57 // or ui::PAGE_TRANSITION_GENERATED. These are weighted twice the engagement of
58 // all other navigations.
59 double kDirectNavigationEngagement
= 1;
60 double kIndirectNavigationEnagagement
= 0.5;
62 scoped_ptr
<base::DictionaryValue
> GetOriginDict(
63 HostContentSettingsMap
* settings
,
64 const GURL
& origin_url
) {
66 return scoped_ptr
<base::DictionaryValue
>();
68 scoped_ptr
<base::Value
> value
= settings
->GetWebsiteSetting(
69 origin_url
, origin_url
, CONTENT_SETTINGS_TYPE_APP_BANNER
, std::string(),
72 return make_scoped_ptr(new base::DictionaryValue());
74 if (!value
->IsType(base::Value::TYPE_DICTIONARY
))
75 return make_scoped_ptr(new base::DictionaryValue());
77 return make_scoped_ptr(static_cast<base::DictionaryValue
*>(value
.release()));
80 base::DictionaryValue
* GetAppDict(base::DictionaryValue
* origin_dict
,
81 const std::string
& key_name
) {
82 base::DictionaryValue
* app_dict
= nullptr;
83 if (!origin_dict
->GetDictionaryWithoutPathExpansion(key_name
, &app_dict
)) {
84 // Don't allow more than kMaxAppsPerSite dictionaries.
85 if (origin_dict
->size() < kMaxAppsPerSite
) {
86 app_dict
= new base::DictionaryValue();
87 origin_dict
->SetWithoutPathExpansion(key_name
, make_scoped_ptr(app_dict
));
94 double GetEventEngagement(ui::PageTransition transition_type
) {
95 if (ui::PageTransitionCoreTypeIs(transition_type
,
96 ui::PAGE_TRANSITION_TYPED
) ||
97 ui::PageTransitionCoreTypeIs(transition_type
,
98 ui::PAGE_TRANSITION_GENERATED
)) {
99 return kDirectNavigationEngagement
;
101 return kIndirectNavigationEnagagement
;
107 void AppBannerSettingsHelper::ClearHistoryForURLs(
109 const std::set
<GURL
>& origin_urls
) {
110 HostContentSettingsMap
* settings
= profile
->GetHostContentSettingsMap();
111 for (const GURL
& origin_url
: origin_urls
) {
112 ContentSettingsPattern
pattern(ContentSettingsPattern::FromURL(origin_url
));
113 if (!pattern
.IsValid())
116 settings
->SetWebsiteSetting(pattern
, ContentSettingsPattern::Wildcard(),
117 CONTENT_SETTINGS_TYPE_APP_BANNER
, std::string(),
119 settings
->FlushLossyWebsiteSettings();
123 void AppBannerSettingsHelper::RecordBannerInstallEvent(
124 content::WebContents
* web_contents
,
125 const std::string
& package_name_or_start_url
,
126 AppBannerRapporMetric rappor_metric
) {
127 banners::TrackInstallEvent(banners::INSTALL_EVENT_WEB_APP_INSTALLED
);
129 AppBannerSettingsHelper::RecordBannerEvent(
130 web_contents
, web_contents
->GetURL(),
131 package_name_or_start_url
,
132 AppBannerSettingsHelper::APP_BANNER_EVENT_DID_ADD_TO_HOMESCREEN
,
133 banners::AppBannerDataFetcher::GetCurrentTime());
135 rappor::SampleDomainAndRegistryFromGURL(
136 g_browser_process
->rappor_service(),
137 (rappor_metric
== WEB
? "AppBanner.WebApp.Installed"
138 : "AppBanner.NativeApp.Installed"),
139 web_contents
->GetURL());
142 void AppBannerSettingsHelper::RecordBannerDismissEvent(
143 content::WebContents
* web_contents
,
144 const std::string
& package_name_or_start_url
,
145 AppBannerRapporMetric rappor_metric
) {
146 banners::TrackDismissEvent(banners::DISMISS_EVENT_CLOSE_BUTTON
);
148 AppBannerSettingsHelper::RecordBannerEvent(
149 web_contents
, web_contents
->GetURL(),
150 package_name_or_start_url
,
151 AppBannerSettingsHelper::APP_BANNER_EVENT_DID_BLOCK
,
152 banners::AppBannerDataFetcher::GetCurrentTime());
154 rappor::SampleDomainAndRegistryFromGURL(
155 g_browser_process
->rappor_service(),
156 (rappor_metric
== WEB
? "AppBanner.WebApp.Dismissed"
157 : "AppBanner.NativeApp.Dismissed"),
158 web_contents
->GetURL());
161 void AppBannerSettingsHelper::RecordBannerEvent(
162 content::WebContents
* web_contents
,
163 const GURL
& origin_url
,
164 const std::string
& package_name_or_start_url
,
165 AppBannerEvent event
,
167 DCHECK(event
!= APP_BANNER_EVENT_COULD_SHOW
);
170 Profile::FromBrowserContext(web_contents
->GetBrowserContext());
171 if (profile
->IsOffTheRecord() || package_name_or_start_url
.empty())
174 ContentSettingsPattern
pattern(ContentSettingsPattern::FromURL(origin_url
));
175 if (!pattern
.IsValid())
178 HostContentSettingsMap
* settings
= profile
->GetHostContentSettingsMap();
179 scoped_ptr
<base::DictionaryValue
> origin_dict
=
180 GetOriginDict(settings
, origin_url
);
184 base::DictionaryValue
* app_dict
=
185 GetAppDict(origin_dict
.get(), package_name_or_start_url
);
189 // Dates are stored in their raw form (i.e. not local dates) to be resilient
190 // to time zone changes.
191 std::string
event_key(kBannerEventKeys
[event
]);
192 app_dict
->SetDouble(event_key
, time
.ToInternalValue());
194 settings
->SetWebsiteSetting(pattern
, ContentSettingsPattern::Wildcard(),
195 CONTENT_SETTINGS_TYPE_APP_BANNER
, std::string(),
196 origin_dict
.release());
198 // App banner content settings are lossy, meaning they will not cause the
199 // prefs to become dirty. This is fine for most events, as if they are lost it
200 // just means the user will have to engage a little bit more. However the
201 // DID_ADD_TO_HOMESCREEN event should always be recorded to prevent
203 if (event
== APP_BANNER_EVENT_DID_ADD_TO_HOMESCREEN
)
204 settings
->FlushLossyWebsiteSettings();
207 void AppBannerSettingsHelper::RecordBannerCouldShowEvent(
208 content::WebContents
* web_contents
,
209 const GURL
& origin_url
,
210 const std::string
& package_name_or_start_url
,
212 ui::PageTransition transition_type
) {
214 Profile::FromBrowserContext(web_contents
->GetBrowserContext());
215 if (profile
->IsOffTheRecord() || package_name_or_start_url
.empty())
218 ContentSettingsPattern
pattern(ContentSettingsPattern::FromURL(origin_url
));
219 if (!pattern
.IsValid())
222 HostContentSettingsMap
* settings
= profile
->GetHostContentSettingsMap();
223 scoped_ptr
<base::DictionaryValue
> origin_dict
=
224 GetOriginDict(settings
, origin_url
);
228 base::DictionaryValue
* app_dict
=
229 GetAppDict(origin_dict
.get(), package_name_or_start_url
);
233 std::string
event_key(kBannerEventKeys
[APP_BANNER_EVENT_COULD_SHOW
]);
234 double engagement
= GetEventEngagement(transition_type
);
236 base::ListValue
* could_show_list
= nullptr;
237 if (!app_dict
->GetList(event_key
, &could_show_list
)) {
238 could_show_list
= new base::ListValue();
239 app_dict
->Set(event_key
, make_scoped_ptr(could_show_list
));
242 // Trim any items that are older than we should care about. For comparisons
243 // the times are converted to local dates.
244 base::Time date
= time
.LocalMidnight();
245 base::ValueVector::iterator it
= could_show_list
->begin();
246 while (it
!= could_show_list
->end()) {
247 if ((*it
)->IsType(base::Value::TYPE_DICTIONARY
)) {
248 base::DictionaryValue
* internal_value
;
249 double internal_date
;
250 (*it
)->GetAsDictionary(&internal_value
);
252 if (internal_value
->GetDouble(kBannerTimeKey
, &internal_date
)) {
253 base::Time other_date
=
254 base::Time::FromInternalValue(internal_date
).LocalMidnight();
255 if (other_date
== date
) {
256 double other_engagement
= 0;
257 if (internal_value
->GetDouble(kBannerEngagementKey
,
258 &other_engagement
) &&
259 other_engagement
>= engagement
) {
260 // This date has already been added, but with an equal or higher
261 // engagement. Don't add the date again. If the conditional fails,
262 // fall to the end of the loop where the existing entry is deleted.
266 base::TimeDelta delta
= date
- other_date
;
268 base::TimeDelta::FromDays(kOldestCouldShowBannerEventInDays
)) {
276 // Either this date is older than we care about, or it isn't in the correct
277 // format, or it is the same as the current date but with a lower
278 // engagement, so remove it.
279 it
= could_show_list
->Erase(it
, nullptr);
282 // Dates are stored in their raw form (i.e. not local dates) to be resilient
283 // to time zone changes.
284 scoped_ptr
<base::DictionaryValue
> value(new base::DictionaryValue());
285 value
->SetDouble(kBannerTimeKey
, time
.ToInternalValue());
286 value
->SetDouble(kBannerEngagementKey
, engagement
);
287 could_show_list
->Append(value
.Pass());
289 settings
->SetWebsiteSetting(pattern
, ContentSettingsPattern::Wildcard(),
290 CONTENT_SETTINGS_TYPE_APP_BANNER
, std::string(),
291 origin_dict
.release());
294 bool AppBannerSettingsHelper::ShouldShowBanner(
295 content::WebContents
* web_contents
,
296 const GURL
& origin_url
,
297 const std::string
& package_name_or_start_url
,
299 // Ignore all checks if the flag to do so is set.
300 if (base::CommandLine::ForCurrentProcess()->HasSwitch(
301 switches::kBypassAppBannerEngagementChecks
)) {
305 // Don't show if it has been added to the homescreen.
306 base::Time added_time
=
307 GetSingleBannerEvent(web_contents
, origin_url
, package_name_or_start_url
,
308 APP_BANNER_EVENT_DID_ADD_TO_HOMESCREEN
);
309 if (!added_time
.is_null()) {
310 banners::TrackDisplayEvent(banners::DISPLAY_EVENT_INSTALLED_PREVIOUSLY
);
314 base::Time blocked_time
=
315 GetSingleBannerEvent(web_contents
, origin_url
, package_name_or_start_url
,
316 APP_BANNER_EVENT_DID_BLOCK
);
318 // Null times are in the distant past, so the delta between real times and
319 // null events will always be greater than the limits.
320 if (time
- blocked_time
<
321 base::TimeDelta::FromDays(kMinimumBannerBlockedToBannerShown
)) {
322 banners::TrackDisplayEvent(banners::DISPLAY_EVENT_BLOCKED_PREVIOUSLY
);
326 base::Time shown_time
=
327 GetSingleBannerEvent(web_contents
, origin_url
, package_name_or_start_url
,
328 APP_BANNER_EVENT_DID_SHOW
);
329 if (time
- shown_time
<
330 base::TimeDelta::FromDays(kMinimumDaysBetweenBannerShows
)) {
331 banners::TrackDisplayEvent(banners::DISPLAY_EVENT_IGNORED_PREVIOUSLY
);
335 std::vector
<BannerEvent
> could_show_events
= GetCouldShowBannerEvents(
336 web_contents
, origin_url
, package_name_or_start_url
);
338 // Return true if the total engagement of each applicable could show event
339 // meets the trigger threshold.
340 double total_engagement
= 0;
341 for (const auto& event
: could_show_events
)
342 total_engagement
+= event
.engagement
;
344 if (total_engagement
< kTotalEngagementToTrigger
) {
345 banners::TrackDisplayEvent(banners::DISPLAY_EVENT_NOT_VISITED_ENOUGH
);
352 std::vector
<AppBannerSettingsHelper::BannerEvent
>
353 AppBannerSettingsHelper::GetCouldShowBannerEvents(
354 content::WebContents
* web_contents
,
355 const GURL
& origin_url
,
356 const std::string
& package_name_or_start_url
) {
357 std::vector
<BannerEvent
> result
;
358 if (package_name_or_start_url
.empty())
362 Profile::FromBrowserContext(web_contents
->GetBrowserContext());
363 HostContentSettingsMap
* settings
= profile
->GetHostContentSettingsMap();
364 scoped_ptr
<base::DictionaryValue
> origin_dict
=
365 GetOriginDict(settings
, origin_url
);
370 base::DictionaryValue
* app_dict
=
371 GetAppDict(origin_dict
.get(), package_name_or_start_url
);
375 std::string
event_key(kBannerEventKeys
[APP_BANNER_EVENT_COULD_SHOW
]);
376 base::ListValue
* could_show_list
= nullptr;
377 if (!app_dict
->GetList(event_key
, &could_show_list
))
380 for (auto value
: *could_show_list
) {
381 if (value
->IsType(base::Value::TYPE_DICTIONARY
)) {
382 base::DictionaryValue
* internal_value
;
383 double internal_date
= 0;
384 value
->GetAsDictionary(&internal_value
);
385 double engagement
= 0;
387 if (internal_value
->GetDouble(kBannerTimeKey
, &internal_date
) &&
388 internal_value
->GetDouble(kBannerEngagementKey
, &engagement
)) {
389 base::Time date
= base::Time::FromInternalValue(internal_date
);
390 result
.push_back({date
, engagement
});
398 base::Time
AppBannerSettingsHelper::GetSingleBannerEvent(
399 content::WebContents
* web_contents
,
400 const GURL
& origin_url
,
401 const std::string
& package_name_or_start_url
,
402 AppBannerEvent event
) {
403 DCHECK(event
!= APP_BANNER_EVENT_COULD_SHOW
);
404 DCHECK(event
< APP_BANNER_EVENT_NUM_EVENTS
);
406 if (package_name_or_start_url
.empty())
410 Profile::FromBrowserContext(web_contents
->GetBrowserContext());
411 HostContentSettingsMap
* settings
= profile
->GetHostContentSettingsMap();
412 scoped_ptr
<base::DictionaryValue
> origin_dict
=
413 GetOriginDict(settings
, origin_url
);
418 base::DictionaryValue
* app_dict
=
419 GetAppDict(origin_dict
.get(), package_name_or_start_url
);
423 std::string
event_key(kBannerEventKeys
[event
]);
424 double internal_time
;
425 if (!app_dict
->GetDouble(event_key
, &internal_time
))
428 return base::Time::FromInternalValue(internal_time
);
431 void AppBannerSettingsHelper::SetEngagementWeights(double direct_engagement
,
432 double indirect_engagement
) {
433 kDirectNavigationEngagement
= direct_engagement
;
434 kIndirectNavigationEnagagement
= indirect_engagement
;