Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / banners / app_banner_settings_helper.cc
blobd4375c8403f634def88431a36ba2c8fed9c94dc2
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 "base/strings/string_number_conversions.h"
12 #include "chrome/browser/banners/app_banner_data_fetcher.h"
13 #include "chrome/browser/banners/app_banner_metrics.h"
14 #include "chrome/browser/browser_process.h"
15 #include "chrome/browser/content_settings/host_content_settings_map_factory.h"
16 #include "chrome/browser/profiles/profile.h"
17 #include "chrome/common/chrome_switches.h"
18 #include "components/content_settings/core/browser/host_content_settings_map.h"
19 #include "components/content_settings/core/common/content_settings_pattern.h"
20 #include "components/rappor/rappor_utils.h"
21 #include "components/variations/variations_associated_data.h"
22 #include "content/public/browser/web_contents.h"
23 #include "net/base/escape.h"
24 #include "url/gurl.h"
26 namespace {
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
45 // for.
46 const unsigned int kMinimumBannerBlockedToBannerShown = 90;
48 // Dictionary keys to use for the events.
49 const char* kBannerEventKeys[] = {
50 "couldShowBannerEvents",
51 "didShowBannerEvent",
52 "didBlockBannerEvent",
53 "didAddToHomescreenEvent",
56 // Keys to use when storing BannerEvent structs.
57 const char kBannerTimeKey[] = "time";
58 const char kBannerEngagementKey[] = "engagement";
60 // Keys to use when querying the variations params.
61 const char kBannerParamsKey[] = "AppBannerTriggering";
62 const char kBannerParamsDirectKey[] = "direct";
63 const char kBannerParamsIndirectKey[] = "indirect";
64 const char kBannerParamsTotalKey[] = "total";
65 const char kBannerParamsMinutesKey[] = "minutes";
67 // Total site engagements where a banner could have been shown before
68 // a banner will actually be triggered.
69 double gTotalEngagementToTrigger = 2;
71 // Engagement weight assigned to direct and indirect navigations.
72 // By default, a direct navigation is a page visit via ui::PAGE_TRANSITION_TYPED
73 // or ui::PAGE_TRANSITION_GENERATED.
74 double gDirectNavigationEngagement = 1;
75 double gIndirectNavigationEnagagement = 1;
77 scoped_ptr<base::DictionaryValue> GetOriginDict(
78 HostContentSettingsMap* settings,
79 const GURL& origin_url) {
80 if (!settings)
81 return scoped_ptr<base::DictionaryValue>();
83 scoped_ptr<base::Value> value = settings->GetWebsiteSetting(
84 origin_url, origin_url, CONTENT_SETTINGS_TYPE_APP_BANNER, std::string(),
85 NULL);
86 if (!value.get())
87 return make_scoped_ptr(new base::DictionaryValue());
89 if (!value->IsType(base::Value::TYPE_DICTIONARY))
90 return make_scoped_ptr(new base::DictionaryValue());
92 return make_scoped_ptr(static_cast<base::DictionaryValue*>(value.release()));
95 base::DictionaryValue* GetAppDict(base::DictionaryValue* origin_dict,
96 const std::string& key_name) {
97 base::DictionaryValue* app_dict = nullptr;
98 if (!origin_dict->GetDictionaryWithoutPathExpansion(key_name, &app_dict)) {
99 // Don't allow more than kMaxAppsPerSite dictionaries.
100 if (origin_dict->size() < kMaxAppsPerSite) {
101 app_dict = new base::DictionaryValue();
102 origin_dict->SetWithoutPathExpansion(key_name, make_scoped_ptr(app_dict));
106 return app_dict;
109 double GetEventEngagement(ui::PageTransition transition_type) {
110 if (ui::PageTransitionCoreTypeIs(transition_type,
111 ui::PAGE_TRANSITION_TYPED) ||
112 ui::PageTransitionCoreTypeIs(transition_type,
113 ui::PAGE_TRANSITION_GENERATED)) {
114 return gDirectNavigationEngagement;
115 } else {
116 return gIndirectNavigationEnagagement;
120 // Queries variations for updates to the default engagement values assigned
121 // to direct and indirect navigations.
122 void UpdateEngagementWeights() {
123 std::map<std::string, std::string> params;
124 std::string direct_param = variations::GetVariationParamValue(
125 kBannerParamsKey, kBannerParamsDirectKey);
126 std::string indirect_param = variations::GetVariationParamValue(
127 kBannerParamsKey, kBannerParamsIndirectKey);
128 std::string total_param = variations::GetVariationParamValue(
129 kBannerParamsKey, kBannerParamsTotalKey);
131 if (!direct_param.empty() && !indirect_param.empty() &&
132 !total_param.empty()) {
133 double direct_engagement = -1;
134 double indirect_engagement = -1;
135 double total_engagement = -1;
137 // Ensure that we get valid doubles from the field trial, and that both
138 // values are greater than or equal to zero and less than or equal to the
139 // total engagement required to trigger the banner.
140 if (base::StringToDouble(direct_param, &direct_engagement) &&
141 base::StringToDouble(indirect_param, &indirect_engagement) &&
142 base::StringToDouble(total_param, &total_engagement) &&
143 direct_engagement >= 0 && indirect_engagement >= 0 &&
144 total_engagement > 0 && direct_engagement <= total_engagement &&
145 indirect_engagement <= total_engagement) {
146 AppBannerSettingsHelper::SetEngagementWeights(direct_engagement,
147 indirect_engagement);
148 AppBannerSettingsHelper::SetTotalEngagementToTrigger(total_engagement);
153 // Queries variation for updates to the default number of minutes between
154 // site visits counted for the purposes of displaying a banner.
155 void UpdateMinutesBetweenVisits() {
156 std::string param = variations::GetVariationParamValue(
157 kBannerParamsKey, kBannerParamsMinutesKey);
158 if (!param.empty()) {
159 int minimum_minutes = 0;
160 if (base::StringToInt(param, &minimum_minutes))
161 AppBannerSettingsHelper::SetMinimumMinutesBetweenVisits(minimum_minutes);
165 } // namespace
167 void AppBannerSettingsHelper::ClearHistoryForURLs(
168 Profile* profile,
169 const std::set<GURL>& origin_urls) {
170 HostContentSettingsMap* settings =
171 HostContentSettingsMapFactory::GetForProfile(profile);
172 for (const GURL& origin_url : origin_urls) {
173 ContentSettingsPattern pattern(ContentSettingsPattern::FromURL(origin_url));
174 if (!pattern.IsValid())
175 continue;
177 settings->SetWebsiteSetting(pattern, ContentSettingsPattern::Wildcard(),
178 CONTENT_SETTINGS_TYPE_APP_BANNER, std::string(),
179 nullptr);
180 settings->FlushLossyWebsiteSettings();
184 void AppBannerSettingsHelper::RecordBannerInstallEvent(
185 content::WebContents* web_contents,
186 const std::string& package_name_or_start_url,
187 AppBannerRapporMetric rappor_metric) {
188 banners::TrackInstallEvent(banners::INSTALL_EVENT_WEB_APP_INSTALLED);
190 AppBannerSettingsHelper::RecordBannerEvent(
191 web_contents, web_contents->GetURL(),
192 package_name_or_start_url,
193 AppBannerSettingsHelper::APP_BANNER_EVENT_DID_ADD_TO_HOMESCREEN,
194 banners::AppBannerDataFetcher::GetCurrentTime());
196 rappor::SampleDomainAndRegistryFromGURL(
197 g_browser_process->rappor_service(),
198 (rappor_metric == WEB ? "AppBanner.WebApp.Installed"
199 : "AppBanner.NativeApp.Installed"),
200 web_contents->GetURL());
203 void AppBannerSettingsHelper::RecordBannerDismissEvent(
204 content::WebContents* web_contents,
205 const std::string& package_name_or_start_url,
206 AppBannerRapporMetric rappor_metric) {
207 banners::TrackDismissEvent(banners::DISMISS_EVENT_CLOSE_BUTTON);
209 AppBannerSettingsHelper::RecordBannerEvent(
210 web_contents, web_contents->GetURL(),
211 package_name_or_start_url,
212 AppBannerSettingsHelper::APP_BANNER_EVENT_DID_BLOCK,
213 banners::AppBannerDataFetcher::GetCurrentTime());
215 rappor::SampleDomainAndRegistryFromGURL(
216 g_browser_process->rappor_service(),
217 (rappor_metric == WEB ? "AppBanner.WebApp.Dismissed"
218 : "AppBanner.NativeApp.Dismissed"),
219 web_contents->GetURL());
222 void AppBannerSettingsHelper::RecordBannerEvent(
223 content::WebContents* web_contents,
224 const GURL& origin_url,
225 const std::string& package_name_or_start_url,
226 AppBannerEvent event,
227 base::Time time) {
228 DCHECK(event != APP_BANNER_EVENT_COULD_SHOW);
230 Profile* profile =
231 Profile::FromBrowserContext(web_contents->GetBrowserContext());
232 if (profile->IsOffTheRecord() || package_name_or_start_url.empty())
233 return;
235 ContentSettingsPattern pattern(ContentSettingsPattern::FromURL(origin_url));
236 if (!pattern.IsValid())
237 return;
239 HostContentSettingsMap* settings =
240 HostContentSettingsMapFactory::GetForProfile(profile);
241 scoped_ptr<base::DictionaryValue> origin_dict =
242 GetOriginDict(settings, origin_url);
243 if (!origin_dict)
244 return;
246 base::DictionaryValue* app_dict =
247 GetAppDict(origin_dict.get(), package_name_or_start_url);
248 if (!app_dict)
249 return;
251 // Dates are stored in their raw form (i.e. not local dates) to be resilient
252 // to time zone changes.
253 std::string event_key(kBannerEventKeys[event]);
254 app_dict->SetDouble(event_key, time.ToInternalValue());
256 settings->SetWebsiteSetting(pattern, ContentSettingsPattern::Wildcard(),
257 CONTENT_SETTINGS_TYPE_APP_BANNER, std::string(),
258 origin_dict.release());
260 // App banner content settings are lossy, meaning they will not cause the
261 // prefs to become dirty. This is fine for most events, as if they are lost it
262 // just means the user will have to engage a little bit more. However the
263 // DID_ADD_TO_HOMESCREEN event should always be recorded to prevent
264 // spamminess.
265 if (event == APP_BANNER_EVENT_DID_ADD_TO_HOMESCREEN)
266 settings->FlushLossyWebsiteSettings();
269 void AppBannerSettingsHelper::RecordBannerCouldShowEvent(
270 content::WebContents* web_contents,
271 const GURL& origin_url,
272 const std::string& package_name_or_start_url,
273 base::Time time,
274 ui::PageTransition transition_type) {
275 Profile* profile =
276 Profile::FromBrowserContext(web_contents->GetBrowserContext());
277 if (profile->IsOffTheRecord() || package_name_or_start_url.empty())
278 return;
280 ContentSettingsPattern pattern(ContentSettingsPattern::FromURL(origin_url));
281 if (!pattern.IsValid())
282 return;
284 HostContentSettingsMap* settings =
285 HostContentSettingsMapFactory::GetForProfile(profile);
286 scoped_ptr<base::DictionaryValue> origin_dict =
287 GetOriginDict(settings, origin_url);
288 if (!origin_dict)
289 return;
291 base::DictionaryValue* app_dict =
292 GetAppDict(origin_dict.get(), package_name_or_start_url);
293 if (!app_dict)
294 return;
296 std::string event_key(kBannerEventKeys[APP_BANNER_EVENT_COULD_SHOW]);
297 double engagement = GetEventEngagement(transition_type);
299 base::ListValue* could_show_list = nullptr;
300 if (!app_dict->GetList(event_key, &could_show_list)) {
301 could_show_list = new base::ListValue();
302 app_dict->Set(event_key, make_scoped_ptr(could_show_list));
305 // Trim any items that are older than we should care about. For comparisons
306 // the times are converted to local dates.
307 base::Time date = BucketTimeToResolution(time, gMinimumMinutesBetweenVisits);
308 base::ValueVector::iterator it = could_show_list->begin();
309 while (it != could_show_list->end()) {
310 if ((*it)->IsType(base::Value::TYPE_DICTIONARY)) {
311 base::DictionaryValue* internal_value;
312 double internal_date;
313 (*it)->GetAsDictionary(&internal_value);
315 if (internal_value->GetDouble(kBannerTimeKey, &internal_date)) {
316 base::Time other_date =
317 BucketTimeToResolution(base::Time::FromInternalValue(internal_date),
318 gMinimumMinutesBetweenVisits);
319 if (other_date == date) {
320 double other_engagement = 0;
321 if (internal_value->GetDouble(kBannerEngagementKey,
322 &other_engagement) &&
323 other_engagement >= engagement) {
324 // This date has already been added, but with an equal or higher
325 // engagement. Don't add the date again. If the conditional fails,
326 // fall to the end of the loop where the existing entry is deleted.
327 return;
329 } else {
330 base::TimeDelta delta = date - other_date;
331 if (delta <
332 base::TimeDelta::FromDays(kOldestCouldShowBannerEventInDays)) {
333 ++it;
334 continue;
340 // Either this date is older than we care about, or it isn't in the correct
341 // format, or it is the same as the current date but with a lower
342 // engagement, so remove it.
343 it = could_show_list->Erase(it, nullptr);
346 // Dates are stored in their raw form (i.e. not local dates) to be resilient
347 // to time zone changes.
348 scoped_ptr<base::DictionaryValue> value(new base::DictionaryValue());
349 value->SetDouble(kBannerTimeKey, time.ToInternalValue());
350 value->SetDouble(kBannerEngagementKey, engagement);
351 could_show_list->Append(value.Pass());
353 settings->SetWebsiteSetting(pattern, ContentSettingsPattern::Wildcard(),
354 CONTENT_SETTINGS_TYPE_APP_BANNER, std::string(),
355 origin_dict.release());
358 bool AppBannerSettingsHelper::ShouldShowBanner(
359 content::WebContents* web_contents,
360 const GURL& origin_url,
361 const std::string& package_name_or_start_url,
362 base::Time time) {
363 // Ignore all checks if the flag to do so is set.
364 if (base::CommandLine::ForCurrentProcess()->HasSwitch(
365 switches::kBypassAppBannerEngagementChecks)) {
366 return true;
369 // Don't show if it has been added to the homescreen.
370 base::Time added_time =
371 GetSingleBannerEvent(web_contents, origin_url, package_name_or_start_url,
372 APP_BANNER_EVENT_DID_ADD_TO_HOMESCREEN);
373 if (!added_time.is_null()) {
374 banners::TrackDisplayEvent(banners::DISPLAY_EVENT_INSTALLED_PREVIOUSLY);
375 return false;
378 base::Time blocked_time =
379 GetSingleBannerEvent(web_contents, origin_url, package_name_or_start_url,
380 APP_BANNER_EVENT_DID_BLOCK);
382 // Null times are in the distant past, so the delta between real times and
383 // null events will always be greater than the limits.
384 if (time - blocked_time <
385 base::TimeDelta::FromDays(kMinimumBannerBlockedToBannerShown)) {
386 banners::TrackDisplayEvent(banners::DISPLAY_EVENT_BLOCKED_PREVIOUSLY);
387 return false;
390 base::Time shown_time =
391 GetSingleBannerEvent(web_contents, origin_url, package_name_or_start_url,
392 APP_BANNER_EVENT_DID_SHOW);
393 if (time - shown_time <
394 base::TimeDelta::FromDays(kMinimumDaysBetweenBannerShows)) {
395 banners::TrackDisplayEvent(banners::DISPLAY_EVENT_IGNORED_PREVIOUSLY);
396 return false;
399 std::vector<BannerEvent> could_show_events = GetCouldShowBannerEvents(
400 web_contents, origin_url, package_name_or_start_url);
402 // Return true if the total engagement of each applicable could show event
403 // meets the trigger threshold.
404 double total_engagement = 0;
405 for (const auto& event : could_show_events)
406 total_engagement += event.engagement;
408 if (total_engagement < gTotalEngagementToTrigger) {
409 banners::TrackDisplayEvent(banners::DISPLAY_EVENT_NOT_VISITED_ENOUGH);
410 return false;
413 return true;
416 std::vector<AppBannerSettingsHelper::BannerEvent>
417 AppBannerSettingsHelper::GetCouldShowBannerEvents(
418 content::WebContents* web_contents,
419 const GURL& origin_url,
420 const std::string& package_name_or_start_url) {
421 std::vector<BannerEvent> result;
422 if (package_name_or_start_url.empty())
423 return result;
425 Profile* profile =
426 Profile::FromBrowserContext(web_contents->GetBrowserContext());
427 HostContentSettingsMap* settings =
428 HostContentSettingsMapFactory::GetForProfile(profile);
429 scoped_ptr<base::DictionaryValue> origin_dict =
430 GetOriginDict(settings, origin_url);
432 if (!origin_dict)
433 return result;
435 base::DictionaryValue* app_dict =
436 GetAppDict(origin_dict.get(), package_name_or_start_url);
437 if (!app_dict)
438 return result;
440 std::string event_key(kBannerEventKeys[APP_BANNER_EVENT_COULD_SHOW]);
441 base::ListValue* could_show_list = nullptr;
442 if (!app_dict->GetList(event_key, &could_show_list))
443 return result;
445 for (auto value : *could_show_list) {
446 if (value->IsType(base::Value::TYPE_DICTIONARY)) {
447 base::DictionaryValue* internal_value;
448 double internal_date = 0;
449 value->GetAsDictionary(&internal_value);
450 double engagement = 0;
452 if (internal_value->GetDouble(kBannerTimeKey, &internal_date) &&
453 internal_value->GetDouble(kBannerEngagementKey, &engagement)) {
454 base::Time date = base::Time::FromInternalValue(internal_date);
455 result.push_back({date, engagement});
460 return result;
463 base::Time AppBannerSettingsHelper::GetSingleBannerEvent(
464 content::WebContents* web_contents,
465 const GURL& origin_url,
466 const std::string& package_name_or_start_url,
467 AppBannerEvent event) {
468 DCHECK(event != APP_BANNER_EVENT_COULD_SHOW);
469 DCHECK(event < APP_BANNER_EVENT_NUM_EVENTS);
471 if (package_name_or_start_url.empty())
472 return base::Time();
474 Profile* profile =
475 Profile::FromBrowserContext(web_contents->GetBrowserContext());
476 HostContentSettingsMap* settings =
477 HostContentSettingsMapFactory::GetForProfile(profile);
478 scoped_ptr<base::DictionaryValue> origin_dict =
479 GetOriginDict(settings, origin_url);
481 if (!origin_dict)
482 return base::Time();
484 base::DictionaryValue* app_dict =
485 GetAppDict(origin_dict.get(), package_name_or_start_url);
486 if (!app_dict)
487 return base::Time();
489 std::string event_key(kBannerEventKeys[event]);
490 double internal_time;
491 if (!app_dict->GetDouble(event_key, &internal_time))
492 return base::Time();
494 return base::Time::FromInternalValue(internal_time);
497 void AppBannerSettingsHelper::SetEngagementWeights(double direct_engagement,
498 double indirect_engagement) {
499 gDirectNavigationEngagement = direct_engagement;
500 gIndirectNavigationEnagagement = indirect_engagement;
503 void AppBannerSettingsHelper::SetMinimumMinutesBetweenVisits(
504 unsigned int minutes) {
505 gMinimumMinutesBetweenVisits = minutes;
508 void AppBannerSettingsHelper::SetTotalEngagementToTrigger(
509 double total_engagement) {
510 gTotalEngagementToTrigger = total_engagement;
513 // Given a time, returns that time scoped to the nearest minute resolution
514 // locally. For example, if the resolution is one hour, this function will
515 // return the time to the closest (previous) hour in the local time zone.
516 base::Time AppBannerSettingsHelper::BucketTimeToResolution(
517 base::Time time,
518 unsigned int minutes) {
519 // Only support resolutions smaller than or equal to one day. Enforce
520 // that resolutions divide evenly into one day. Otherwise, default to a
521 // day resolution (each time converted to midnight local time).
522 if (minutes == 0 || minutes >= kNumberOfMinutesInADay ||
523 kNumberOfMinutesInADay % minutes != 0) {
524 return time.LocalMidnight();
527 // Extract the number of minutes past midnight in local time. Divide that
528 // number by the resolution size, and return the time converted to local
529 // midnight with the resulting truncated number added.
530 base::Time::Exploded exploded;
531 time.LocalExplode(&exploded);
532 int total_minutes = exploded.hour * 60 + exploded.minute;
534 // Use truncating integer division here.
535 return time.LocalMidnight() +
536 base::TimeDelta::FromMinutes((total_minutes / minutes) * minutes);
539 void AppBannerSettingsHelper::UpdateFromFieldTrial() {
540 UpdateEngagementWeights();
541 UpdateMinutesBetweenVisits();