ExtensionSyncService cleanup: process uninstalls in one step
[chromium-blink-merge.git] / chrome / browser / banners / app_banner_settings_helper.cc
blob2f5603cb97854440b2f15d5768bac61cf275498f
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 // 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) {
63 if (!settings)
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(),
68 NULL);
69 if (!value.get())
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));
89 return 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;
98 } else {
99 return kIndirectNavigationEnagagement;
103 } // namespace
105 void AppBannerSettingsHelper::ClearHistoryForURLs(
106 Profile* profile,
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())
112 continue;
114 settings->SetWebsiteSetting(pattern, ContentSettingsPattern::Wildcard(),
115 CONTENT_SETTINGS_TYPE_APP_BANNER, std::string(),
116 nullptr);
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,
164 base::Time time) {
165 DCHECK(event != APP_BANNER_EVENT_COULD_SHOW);
167 Profile* profile =
168 Profile::FromBrowserContext(web_contents->GetBrowserContext());
169 if (profile->IsOffTheRecord() || package_name_or_start_url.empty())
170 return;
172 ContentSettingsPattern pattern(ContentSettingsPattern::FromURL(origin_url));
173 if (!pattern.IsValid())
174 return;
176 HostContentSettingsMap* settings = profile->GetHostContentSettingsMap();
177 scoped_ptr<base::DictionaryValue> origin_dict =
178 GetOriginDict(settings, origin_url);
179 if (!origin_dict)
180 return;
182 base::DictionaryValue* app_dict =
183 GetAppDict(origin_dict.get(), package_name_or_start_url);
184 if (!app_dict)
185 return;
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
200 // spamminess.
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,
209 base::Time time,
210 ui::PageTransition transition_type) {
211 Profile* profile =
212 Profile::FromBrowserContext(web_contents->GetBrowserContext());
213 if (profile->IsOffTheRecord() || package_name_or_start_url.empty())
214 return;
216 ContentSettingsPattern pattern(ContentSettingsPattern::FromURL(origin_url));
217 if (!pattern.IsValid())
218 return;
220 HostContentSettingsMap* settings = profile->GetHostContentSettingsMap();
221 scoped_ptr<base::DictionaryValue> origin_dict =
222 GetOriginDict(settings, origin_url);
223 if (!origin_dict)
224 return;
226 base::DictionaryValue* app_dict =
227 GetAppDict(origin_dict.get(), package_name_or_start_url);
228 if (!app_dict)
229 return;
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.
261 return;
263 } else {
264 base::TimeDelta delta = date - other_date;
265 if (delta <
266 base::TimeDelta::FromDays(kOldestCouldShowBannerEventInDays)) {
267 ++it;
268 continue;
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,
296 base::Time time) {
297 // Ignore all checks if the flag to do so is set.
298 if (base::CommandLine::ForCurrentProcess()->HasSwitch(
299 switches::kBypassAppBannerEngagementChecks)) {
300 return true;
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);
309 return false;
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);
321 return false;
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);
330 return false;
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);
344 return false;
347 return true;
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())
357 return result;
359 Profile* profile =
360 Profile::FromBrowserContext(web_contents->GetBrowserContext());
361 HostContentSettingsMap* settings = profile->GetHostContentSettingsMap();
362 scoped_ptr<base::DictionaryValue> origin_dict =
363 GetOriginDict(settings, origin_url);
365 if (!origin_dict)
366 return result;
368 base::DictionaryValue* app_dict =
369 GetAppDict(origin_dict.get(), package_name_or_start_url);
370 if (!app_dict)
371 return result;
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))
376 return result;
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});
393 return result;
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())
405 return base::Time();
407 Profile* profile =
408 Profile::FromBrowserContext(web_contents->GetBrowserContext());
409 HostContentSettingsMap* settings = profile->GetHostContentSettingsMap();
410 scoped_ptr<base::DictionaryValue> origin_dict =
411 GetOriginDict(settings, origin_url);
413 if (!origin_dict)
414 return base::Time();
416 base::DictionaryValue* app_dict =
417 GetAppDict(origin_dict.get(), package_name_or_start_url);
418 if (!app_dict)
419 return base::Time();
421 std::string event_key(kBannerEventKeys[event]);
422 double internal_time;
423 if (!app_dict->GetDouble(event_key, &internal_time))
424 return base::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;