Only grant permissions to new extensions from sync if they have the expected version
[chromium-blink-merge.git] / chrome / browser / banners / app_banner_settings_helper.cc
blobe4dcf7d6725bb6ac822b3763e2a0dd5b2621e6a1
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/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"
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 // 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) {
73 if (!settings)
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(),
78 NULL);
79 if (!value.get())
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));
99 return 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;
108 } else {
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");
121 if (weights.empty())
122 return;
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())
153 return;
155 int minimum_minutes = 0;
156 if (base::StringToInt(minutes_between_visits, &minimum_minutes))
157 AppBannerSettingsHelper::SetMinimumMinutesBetweenVisits(minimum_minutes);
160 } // namespace
162 void AppBannerSettingsHelper::ClearHistoryForURLs(
163 Profile* profile,
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())
169 continue;
171 settings->SetWebsiteSetting(pattern, ContentSettingsPattern::Wildcard(),
172 CONTENT_SETTINGS_TYPE_APP_BANNER, std::string(),
173 nullptr);
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,
221 base::Time time) {
222 DCHECK(event != APP_BANNER_EVENT_COULD_SHOW);
224 Profile* profile =
225 Profile::FromBrowserContext(web_contents->GetBrowserContext());
226 if (profile->IsOffTheRecord() || package_name_or_start_url.empty())
227 return;
229 ContentSettingsPattern pattern(ContentSettingsPattern::FromURL(origin_url));
230 if (!pattern.IsValid())
231 return;
233 HostContentSettingsMap* settings = profile->GetHostContentSettingsMap();
234 scoped_ptr<base::DictionaryValue> origin_dict =
235 GetOriginDict(settings, origin_url);
236 if (!origin_dict)
237 return;
239 base::DictionaryValue* app_dict =
240 GetAppDict(origin_dict.get(), package_name_or_start_url);
241 if (!app_dict)
242 return;
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
257 // spamminess.
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,
266 base::Time time,
267 ui::PageTransition transition_type) {
268 Profile* profile =
269 Profile::FromBrowserContext(web_contents->GetBrowserContext());
270 if (profile->IsOffTheRecord() || package_name_or_start_url.empty())
271 return;
273 ContentSettingsPattern pattern(ContentSettingsPattern::FromURL(origin_url));
274 if (!pattern.IsValid())
275 return;
277 HostContentSettingsMap* settings = profile->GetHostContentSettingsMap();
278 scoped_ptr<base::DictionaryValue> origin_dict =
279 GetOriginDict(settings, origin_url);
280 if (!origin_dict)
281 return;
283 base::DictionaryValue* app_dict =
284 GetAppDict(origin_dict.get(), package_name_or_start_url);
285 if (!app_dict)
286 return;
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.
319 return;
321 } else {
322 base::TimeDelta delta = date - other_date;
323 if (delta <
324 base::TimeDelta::FromDays(kOldestCouldShowBannerEventInDays)) {
325 ++it;
326 continue;
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,
354 base::Time time) {
355 // Ignore all checks if the flag to do so is set.
356 if (base::CommandLine::ForCurrentProcess()->HasSwitch(
357 switches::kBypassAppBannerEngagementChecks)) {
358 return true;
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);
367 return false;
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);
379 return false;
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);
388 return false;
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);
402 return false;
405 return true;
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())
415 return result;
417 Profile* profile =
418 Profile::FromBrowserContext(web_contents->GetBrowserContext());
419 HostContentSettingsMap* settings = profile->GetHostContentSettingsMap();
420 scoped_ptr<base::DictionaryValue> origin_dict =
421 GetOriginDict(settings, origin_url);
423 if (!origin_dict)
424 return result;
426 base::DictionaryValue* app_dict =
427 GetAppDict(origin_dict.get(), package_name_or_start_url);
428 if (!app_dict)
429 return result;
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))
434 return result;
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});
451 return result;
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())
463 return base::Time();
465 Profile* profile =
466 Profile::FromBrowserContext(web_contents->GetBrowserContext());
467 HostContentSettingsMap* settings = profile->GetHostContentSettingsMap();
468 scoped_ptr<base::DictionaryValue> origin_dict =
469 GetOriginDict(settings, origin_url);
471 if (!origin_dict)
472 return base::Time();
474 base::DictionaryValue* app_dict =
475 GetAppDict(origin_dict.get(), package_name_or_start_url);
476 if (!app_dict)
477 return base::Time();
479 std::string event_key(kBannerEventKeys[event]);
480 double internal_time;
481 if (!app_dict->GetDouble(event_key, &internal_time))
482 return base::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(
507 base::Time time,
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();