[sql] Remove _HAS_EXCEPTIONS=0 from build info.
[chromium-blink-merge.git] / chrome / browser / banners / app_banner_settings_helper.cc
blob88954441cc67fa8217c80aff8b8950d04e47c055
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"
7 #include <algorithm>
8 #include <string>
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"
21 #include "url/gurl.h"
23 namespace {
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
40 // for.
41 const unsigned int kMinimumBannerBlockedToBannerShown = 90;
43 // Dictionary keys to use for the events.
44 const char* kBannerEventKeys[] = {
45 "couldShowBannerEvents",
46 "didShowBannerEvent",
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) {
65 if (!settings)
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(),
70 NULL);
71 if (!value.get())
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));
91 return 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;
100 } else {
101 return kIndirectNavigationEnagagement;
105 } // namespace
107 void AppBannerSettingsHelper::ClearHistoryForURLs(
108 Profile* profile,
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())
114 continue;
116 settings->SetWebsiteSetting(pattern, ContentSettingsPattern::Wildcard(),
117 CONTENT_SETTINGS_TYPE_APP_BANNER, std::string(),
118 nullptr);
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,
166 base::Time time) {
167 DCHECK(event != APP_BANNER_EVENT_COULD_SHOW);
169 Profile* profile =
170 Profile::FromBrowserContext(web_contents->GetBrowserContext());
171 if (profile->IsOffTheRecord() || package_name_or_start_url.empty())
172 return;
174 ContentSettingsPattern pattern(ContentSettingsPattern::FromURL(origin_url));
175 if (!pattern.IsValid())
176 return;
178 HostContentSettingsMap* settings = profile->GetHostContentSettingsMap();
179 scoped_ptr<base::DictionaryValue> origin_dict =
180 GetOriginDict(settings, origin_url);
181 if (!origin_dict)
182 return;
184 base::DictionaryValue* app_dict =
185 GetAppDict(origin_dict.get(), package_name_or_start_url);
186 if (!app_dict)
187 return;
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
202 // spamminess.
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,
211 base::Time time,
212 ui::PageTransition transition_type) {
213 Profile* profile =
214 Profile::FromBrowserContext(web_contents->GetBrowserContext());
215 if (profile->IsOffTheRecord() || package_name_or_start_url.empty())
216 return;
218 ContentSettingsPattern pattern(ContentSettingsPattern::FromURL(origin_url));
219 if (!pattern.IsValid())
220 return;
222 HostContentSettingsMap* settings = profile->GetHostContentSettingsMap();
223 scoped_ptr<base::DictionaryValue> origin_dict =
224 GetOriginDict(settings, origin_url);
225 if (!origin_dict)
226 return;
228 base::DictionaryValue* app_dict =
229 GetAppDict(origin_dict.get(), package_name_or_start_url);
230 if (!app_dict)
231 return;
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.
263 return;
265 } else {
266 base::TimeDelta delta = date - other_date;
267 if (delta <
268 base::TimeDelta::FromDays(kOldestCouldShowBannerEventInDays)) {
269 ++it;
270 continue;
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,
298 base::Time time) {
299 // Ignore all checks if the flag to do so is set.
300 if (base::CommandLine::ForCurrentProcess()->HasSwitch(
301 switches::kBypassAppBannerEngagementChecks)) {
302 return true;
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);
311 return false;
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);
323 return false;
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);
332 return false;
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);
346 return false;
349 return true;
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())
359 return result;
361 Profile* profile =
362 Profile::FromBrowserContext(web_contents->GetBrowserContext());
363 HostContentSettingsMap* settings = profile->GetHostContentSettingsMap();
364 scoped_ptr<base::DictionaryValue> origin_dict =
365 GetOriginDict(settings, origin_url);
367 if (!origin_dict)
368 return result;
370 base::DictionaryValue* app_dict =
371 GetAppDict(origin_dict.get(), package_name_or_start_url);
372 if (!app_dict)
373 return result;
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))
378 return result;
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});
395 return result;
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())
407 return base::Time();
409 Profile* profile =
410 Profile::FromBrowserContext(web_contents->GetBrowserContext());
411 HostContentSettingsMap* settings = profile->GetHostContentSettingsMap();
412 scoped_ptr<base::DictionaryValue> origin_dict =
413 GetOriginDict(settings, origin_url);
415 if (!origin_dict)
416 return base::Time();
418 base::DictionaryValue* app_dict =
419 GetAppDict(origin_dict.get(), package_name_or_start_url);
420 if (!app_dict)
421 return base::Time();
423 std::string event_key(kBannerEventKeys[event]);
424 double internal_time;
425 if (!app_dict->GetDouble(event_key, &internal_time))
426 return base::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;