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 // TODO(dominickn) make direct enagagements worth more than indirect by default.
57 double kDirectNavigationEngagement
= 1;
58 double kIndirectNavigationEnagagement
= 1;
60 scoped_ptr
<base::DictionaryValue
> GetOriginDict(
61 HostContentSettingsMap
* settings
,
62 const GURL
& origin_url
) {
64 return scoped_ptr
<base::DictionaryValue
>();
66 scoped_ptr
<base::Value
> value
= settings
->GetWebsiteSetting(
67 origin_url
, origin_url
, CONTENT_SETTINGS_TYPE_APP_BANNER
, std::string(),
70 return make_scoped_ptr(new base::DictionaryValue());
72 if (!value
->IsType(base::Value::TYPE_DICTIONARY
))
73 return make_scoped_ptr(new base::DictionaryValue());
75 return make_scoped_ptr(static_cast<base::DictionaryValue
*>(value
.release()));
78 base::DictionaryValue
* GetAppDict(base::DictionaryValue
* origin_dict
,
79 const std::string
& key_name
) {
80 base::DictionaryValue
* app_dict
= nullptr;
81 if (!origin_dict
->GetDictionaryWithoutPathExpansion(key_name
, &app_dict
)) {
82 // Don't allow more than kMaxAppsPerSite dictionaries.
83 if (origin_dict
->size() < kMaxAppsPerSite
) {
84 app_dict
= new base::DictionaryValue();
85 origin_dict
->SetWithoutPathExpansion(key_name
, make_scoped_ptr(app_dict
));
92 double GetEventEngagement(ui::PageTransition transition_type
) {
93 if (ui::PageTransitionCoreTypeIs(transition_type
,
94 ui::PAGE_TRANSITION_TYPED
) ||
95 ui::PageTransitionCoreTypeIs(transition_type
,
96 ui::PAGE_TRANSITION_GENERATED
)) {
97 return kDirectNavigationEngagement
;
99 return kIndirectNavigationEnagagement
;
105 void AppBannerSettingsHelper::ClearHistoryForURLs(
107 const std::set
<GURL
>& origin_urls
) {
108 HostContentSettingsMap
* settings
= profile
->GetHostContentSettingsMap();
109 for (const GURL
& origin_url
: origin_urls
) {
110 ContentSettingsPattern
pattern(ContentSettingsPattern::FromURL(origin_url
));
111 if (!pattern
.IsValid())
114 settings
->SetWebsiteSetting(pattern
, ContentSettingsPattern::Wildcard(),
115 CONTENT_SETTINGS_TYPE_APP_BANNER
, std::string(),
117 settings
->FlushLossyWebsiteSettings();
121 void AppBannerSettingsHelper::RecordBannerInstallEvent(
122 content::WebContents
* web_contents
,
123 const std::string
& package_name_or_start_url
,
124 AppBannerRapporMetric rappor_metric
) {
125 banners::TrackInstallEvent(banners::INSTALL_EVENT_WEB_APP_INSTALLED
);
127 AppBannerSettingsHelper::RecordBannerEvent(
128 web_contents
, web_contents
->GetURL(),
129 package_name_or_start_url
,
130 AppBannerSettingsHelper::APP_BANNER_EVENT_DID_ADD_TO_HOMESCREEN
,
131 banners::AppBannerDataFetcher::GetCurrentTime());
133 rappor::SampleDomainAndRegistryFromGURL(
134 g_browser_process
->rappor_service(),
135 (rappor_metric
== WEB
? "AppBanner.WebApp.Installed"
136 : "AppBanner.NativeApp.Installed"),
137 web_contents
->GetURL());
140 void AppBannerSettingsHelper::RecordBannerDismissEvent(
141 content::WebContents
* web_contents
,
142 const std::string
& package_name_or_start_url
,
143 AppBannerRapporMetric rappor_metric
) {
144 banners::TrackDismissEvent(banners::DISMISS_EVENT_CLOSE_BUTTON
);
146 AppBannerSettingsHelper::RecordBannerEvent(
147 web_contents
, web_contents
->GetURL(),
148 package_name_or_start_url
,
149 AppBannerSettingsHelper::APP_BANNER_EVENT_DID_BLOCK
,
150 banners::AppBannerDataFetcher::GetCurrentTime());
152 rappor::SampleDomainAndRegistryFromGURL(
153 g_browser_process
->rappor_service(),
154 (rappor_metric
== WEB
? "AppBanner.WebApp.Dismissed"
155 : "AppBanner.NativeApp.Dismissed"),
156 web_contents
->GetURL());
159 void AppBannerSettingsHelper::RecordBannerEvent(
160 content::WebContents
* web_contents
,
161 const GURL
& origin_url
,
162 const std::string
& package_name_or_start_url
,
163 AppBannerEvent event
,
165 DCHECK(event
!= APP_BANNER_EVENT_COULD_SHOW
);
168 Profile::FromBrowserContext(web_contents
->GetBrowserContext());
169 if (profile
->IsOffTheRecord() || package_name_or_start_url
.empty())
172 ContentSettingsPattern
pattern(ContentSettingsPattern::FromURL(origin_url
));
173 if (!pattern
.IsValid())
176 HostContentSettingsMap
* settings
= profile
->GetHostContentSettingsMap();
177 scoped_ptr
<base::DictionaryValue
> origin_dict
=
178 GetOriginDict(settings
, origin_url
);
182 base::DictionaryValue
* app_dict
=
183 GetAppDict(origin_dict
.get(), package_name_or_start_url
);
187 // Dates are stored in their raw form (i.e. not local dates) to be resilient
188 // to time zone changes.
189 std::string
event_key(kBannerEventKeys
[event
]);
190 app_dict
->SetDouble(event_key
, time
.ToInternalValue());
192 settings
->SetWebsiteSetting(pattern
, ContentSettingsPattern::Wildcard(),
193 CONTENT_SETTINGS_TYPE_APP_BANNER
, std::string(),
194 origin_dict
.release());
196 // App banner content settings are lossy, meaning they will not cause the
197 // prefs to become dirty. This is fine for most events, as if they are lost it
198 // just means the user will have to engage a little bit more. However the
199 // DID_ADD_TO_HOMESCREEN event should always be recorded to prevent
201 if (event
== APP_BANNER_EVENT_DID_ADD_TO_HOMESCREEN
)
202 settings
->FlushLossyWebsiteSettings();
205 void AppBannerSettingsHelper::RecordBannerCouldShowEvent(
206 content::WebContents
* web_contents
,
207 const GURL
& origin_url
,
208 const std::string
& package_name_or_start_url
,
210 ui::PageTransition transition_type
) {
212 Profile::FromBrowserContext(web_contents
->GetBrowserContext());
213 if (profile
->IsOffTheRecord() || package_name_or_start_url
.empty())
216 ContentSettingsPattern
pattern(ContentSettingsPattern::FromURL(origin_url
));
217 if (!pattern
.IsValid())
220 HostContentSettingsMap
* settings
= profile
->GetHostContentSettingsMap();
221 scoped_ptr
<base::DictionaryValue
> origin_dict
=
222 GetOriginDict(settings
, origin_url
);
226 base::DictionaryValue
* app_dict
=
227 GetAppDict(origin_dict
.get(), package_name_or_start_url
);
231 std::string
event_key(kBannerEventKeys
[APP_BANNER_EVENT_COULD_SHOW
]);
232 double engagement
= GetEventEngagement(transition_type
);
234 base::ListValue
* could_show_list
= nullptr;
235 if (!app_dict
->GetList(event_key
, &could_show_list
)) {
236 could_show_list
= new base::ListValue();
237 app_dict
->Set(event_key
, make_scoped_ptr(could_show_list
));
240 // Trim any items that are older than we should care about. For comparisons
241 // the times are converted to local dates.
242 base::Time date
= time
.LocalMidnight();
243 base::ValueVector::iterator it
= could_show_list
->begin();
244 while (it
!= could_show_list
->end()) {
245 if ((*it
)->IsType(base::Value::TYPE_DICTIONARY
)) {
246 base::DictionaryValue
* internal_value
;
247 double internal_date
;
248 (*it
)->GetAsDictionary(&internal_value
);
250 if (internal_value
->GetDouble(kBannerTimeKey
, &internal_date
)) {
251 base::Time other_date
=
252 base::Time::FromInternalValue(internal_date
).LocalMidnight();
253 if (other_date
== date
) {
254 double other_engagement
= 0;
255 if (internal_value
->GetDouble(kBannerEngagementKey
,
256 &other_engagement
) &&
257 other_engagement
>= engagement
) {
258 // This date has already been added, but with an equal or higher
259 // engagement. Don't add the date again. If the conditional fails,
260 // fall to the end of the loop where the existing entry is deleted.
264 base::TimeDelta delta
= date
- other_date
;
266 base::TimeDelta::FromDays(kOldestCouldShowBannerEventInDays
)) {
274 // Either this date is older than we care about, or it isn't in the correct
275 // format, or it is the same as the current date but with a lower
276 // engagement, so remove it.
277 it
= could_show_list
->Erase(it
, nullptr);
280 // Dates are stored in their raw form (i.e. not local dates) to be resilient
281 // to time zone changes.
282 scoped_ptr
<base::DictionaryValue
> value(new base::DictionaryValue());
283 value
->SetDouble(kBannerTimeKey
, time
.ToInternalValue());
284 value
->SetDouble(kBannerEngagementKey
, engagement
);
285 could_show_list
->Append(value
.Pass());
287 settings
->SetWebsiteSetting(pattern
, ContentSettingsPattern::Wildcard(),
288 CONTENT_SETTINGS_TYPE_APP_BANNER
, std::string(),
289 origin_dict
.release());
292 bool AppBannerSettingsHelper::ShouldShowBanner(
293 content::WebContents
* web_contents
,
294 const GURL
& origin_url
,
295 const std::string
& package_name_or_start_url
,
297 // Ignore all checks if the flag to do so is set.
298 if (base::CommandLine::ForCurrentProcess()->HasSwitch(
299 switches::kBypassAppBannerEngagementChecks
)) {
303 // Don't show if it has been added to the homescreen.
304 base::Time added_time
=
305 GetSingleBannerEvent(web_contents
, origin_url
, package_name_or_start_url
,
306 APP_BANNER_EVENT_DID_ADD_TO_HOMESCREEN
);
307 if (!added_time
.is_null()) {
308 banners::TrackDisplayEvent(banners::DISPLAY_EVENT_INSTALLED_PREVIOUSLY
);
312 base::Time blocked_time
=
313 GetSingleBannerEvent(web_contents
, origin_url
, package_name_or_start_url
,
314 APP_BANNER_EVENT_DID_BLOCK
);
316 // Null times are in the distant past, so the delta between real times and
317 // null events will always be greater than the limits.
318 if (time
- blocked_time
<
319 base::TimeDelta::FromDays(kMinimumBannerBlockedToBannerShown
)) {
320 banners::TrackDisplayEvent(banners::DISPLAY_EVENT_BLOCKED_PREVIOUSLY
);
324 base::Time shown_time
=
325 GetSingleBannerEvent(web_contents
, origin_url
, package_name_or_start_url
,
326 APP_BANNER_EVENT_DID_SHOW
);
327 if (time
- shown_time
<
328 base::TimeDelta::FromDays(kMinimumDaysBetweenBannerShows
)) {
329 banners::TrackDisplayEvent(banners::DISPLAY_EVENT_IGNORED_PREVIOUSLY
);
333 std::vector
<BannerEvent
> could_show_events
= GetCouldShowBannerEvents(
334 web_contents
, origin_url
, package_name_or_start_url
);
336 // Return true if the total engagement of each applicable could show event
337 // meets the trigger threshold.
338 double total_engagement
= 0;
339 for (const auto& event
: could_show_events
)
340 total_engagement
+= event
.engagement
;
342 if (total_engagement
< kTotalEngagementToTrigger
) {
343 banners::TrackDisplayEvent(banners::DISPLAY_EVENT_NOT_VISITED_ENOUGH
);
350 std::vector
<AppBannerSettingsHelper::BannerEvent
>
351 AppBannerSettingsHelper::GetCouldShowBannerEvents(
352 content::WebContents
* web_contents
,
353 const GURL
& origin_url
,
354 const std::string
& package_name_or_start_url
) {
355 std::vector
<BannerEvent
> result
;
356 if (package_name_or_start_url
.empty())
360 Profile::FromBrowserContext(web_contents
->GetBrowserContext());
361 HostContentSettingsMap
* settings
= profile
->GetHostContentSettingsMap();
362 scoped_ptr
<base::DictionaryValue
> origin_dict
=
363 GetOriginDict(settings
, origin_url
);
368 base::DictionaryValue
* app_dict
=
369 GetAppDict(origin_dict
.get(), package_name_or_start_url
);
373 std::string
event_key(kBannerEventKeys
[APP_BANNER_EVENT_COULD_SHOW
]);
374 base::ListValue
* could_show_list
= nullptr;
375 if (!app_dict
->GetList(event_key
, &could_show_list
))
378 for (auto value
: *could_show_list
) {
379 if (value
->IsType(base::Value::TYPE_DICTIONARY
)) {
380 base::DictionaryValue
* internal_value
;
381 double internal_date
= 0;
382 value
->GetAsDictionary(&internal_value
);
383 double engagement
= 0;
385 if (internal_value
->GetDouble(kBannerTimeKey
, &internal_date
) &&
386 internal_value
->GetDouble(kBannerEngagementKey
, &engagement
)) {
387 base::Time date
= base::Time::FromInternalValue(internal_date
);
388 result
.push_back({date
, engagement
});
396 base::Time
AppBannerSettingsHelper::GetSingleBannerEvent(
397 content::WebContents
* web_contents
,
398 const GURL
& origin_url
,
399 const std::string
& package_name_or_start_url
,
400 AppBannerEvent event
) {
401 DCHECK(event
!= APP_BANNER_EVENT_COULD_SHOW
);
402 DCHECK(event
< APP_BANNER_EVENT_NUM_EVENTS
);
404 if (package_name_or_start_url
.empty())
408 Profile::FromBrowserContext(web_contents
->GetBrowserContext());
409 HostContentSettingsMap
* settings
= profile
->GetHostContentSettingsMap();
410 scoped_ptr
<base::DictionaryValue
> origin_dict
=
411 GetOriginDict(settings
, origin_url
);
416 base::DictionaryValue
* app_dict
=
417 GetAppDict(origin_dict
.get(), package_name_or_start_url
);
421 std::string
event_key(kBannerEventKeys
[event
]);
422 double internal_time
;
423 if (!app_dict
->GetDouble(event_key
, &internal_time
))
426 return base::Time::FromInternalValue(internal_time
);
429 void AppBannerSettingsHelper::SetEngagementWeights(double direct_engagement
,
430 double indirect_engagement
) {
431 kDirectNavigationEngagement
= direct_engagement
;
432 kIndirectNavigationEnagagement
= indirect_engagement
;