1 // Copyright (c) 2012 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/web_resource/notification_promo.h"
10 #include "base/bind.h"
11 #include "base/prefs/pref_registry_simple.h"
12 #include "base/prefs/pref_service.h"
13 #include "base/rand_util.h"
14 #include "base/strings/string_number_conversions.h"
15 #include "base/strings/string_util.h"
16 #include "base/sys_info.h"
17 #include "base/threading/thread_restrictions.h"
18 #include "base/time/time.h"
19 #include "base/values.h"
20 #include "chrome/browser/browser_process.h"
21 #include "chrome/browser/web_resource/promo_resource_service.h"
22 #include "chrome/common/chrome_version_info.h"
23 #include "chrome/common/pref_names.h"
24 #include "components/pref_registry/pref_registry_syncable.h"
25 #include "content/public/browser/user_metrics.h"
26 #include "net/base/url_util.h"
27 #include "ui/base/device_form_factor.h"
30 using base::UserMetricsAction
;
34 const int kDefaultGroupSize
= 100;
36 const char promo_server_url
[] = "https://clients3.google.com/crsignal/client";
38 // The name of the preference that stores the promotion object.
39 const char kPrefPromoObject
[] = "promo";
41 // Keys in the kPrefPromoObject dictionary; used only here.
42 const char kPrefPromoText
[] = "text";
43 const char kPrefPromoPayload
[] = "payload";
44 const char kPrefPromoStart
[] = "start";
45 const char kPrefPromoEnd
[] = "end";
46 const char kPrefPromoNumGroups
[] = "num_groups";
47 const char kPrefPromoSegment
[] = "segment";
48 const char kPrefPromoIncrement
[] = "increment";
49 const char kPrefPromoIncrementFrequency
[] = "increment_frequency";
50 const char kPrefPromoIncrementMax
[] = "increment_max";
51 const char kPrefPromoMaxViews
[] = "max_views";
52 const char kPrefPromoMaxSeconds
[] = "max_seconds";
53 const char kPrefPromoFirstViewTime
[] = "first_view_time";
54 const char kPrefPromoGroup
[] = "group";
55 const char kPrefPromoViews
[] = "views";
56 const char kPrefPromoClosed
[] = "closed";
58 // Returns a string suitable for the Promo Server URL 'osname' value.
59 std::string
PlatformString() {
62 #elif defined(OS_ANDROID)
63 ui::DeviceFormFactor form_factor
= ui::GetDeviceFormFactor();
64 return std::string("android-") +
65 (form_factor
== ui::DEVICE_FORM_FACTOR_TABLET
? "tablet" : "phone");
67 ui::DeviceFormFactor form_factor
= ui::GetDeviceFormFactor();
68 return std::string("ios-") +
69 (form_factor
== ui::DEVICE_FORM_FACTOR_TABLET
? "tablet" : "phone");
70 #elif defined(OS_MACOSX)
72 #elif defined(OS_CHROMEOS)
74 #elif defined(OS_LINUX)
81 // Returns a string suitable for the Promo Server URL 'dist' value.
82 const char* ChannelString() {
84 // GetChannel hits the registry on Windows. See http://crbug.com/70898.
85 // TODO(achuith): Move NotificationPromo::PromoServerURL to the blocking pool.
86 base::ThreadRestrictions::ScopedAllowIO allow_io
;
88 const chrome::VersionInfo::Channel channel
=
89 chrome::VersionInfo::GetChannel();
91 case chrome::VersionInfo::CHANNEL_CANARY
:
93 case chrome::VersionInfo::CHANNEL_DEV
:
95 case chrome::VersionInfo::CHANNEL_BETA
:
97 case chrome::VersionInfo::CHANNEL_STABLE
:
104 struct PromoMapEntry
{
105 NotificationPromo::PromoType promo_type
;
106 const char* promo_type_str
;
109 const PromoMapEntry kPromoMap
[] = {
110 { NotificationPromo::NO_PROMO
, "" },
111 { NotificationPromo::NTP_NOTIFICATION_PROMO
, "ntp_notification_promo" },
112 { NotificationPromo::NTP_BUBBLE_PROMO
, "ntp_bubble_promo" },
113 { NotificationPromo::MOBILE_NTP_SYNC_PROMO
, "mobile_ntp_sync_promo" },
114 { NotificationPromo::MOBILE_NTP_WHATS_NEW_PROMO
,
115 "mobile_ntp_whats_new_promo" },
118 // Convert PromoType to appropriate string.
119 const char* PromoTypeToString(NotificationPromo::PromoType promo_type
) {
120 for (size_t i
= 0; i
< arraysize(kPromoMap
); ++i
) {
121 if (kPromoMap
[i
].promo_type
== promo_type
)
122 return kPromoMap
[i
].promo_type_str
;
128 // Deep-copies a node, replacing any "value" that is a key
129 // into "strings" dictionary with its value from "strings".
131 // {promo_action_args:['MSG_SHORT']} + strings:{MSG_SHORT:'yes'}
133 // {promo_action_args:['yes']}
134 // |node| - a value to be deep copied and resolved.
135 // |strings| - a dictionary of strings to be used for resolution.
136 // Returns a _new_ object that is a deep copy with replacements.
137 // TODO(aruslan): http://crbug.com/144320 Consider moving it to values.cc/h.
138 base::Value
* DeepCopyAndResolveStrings(
139 const base::Value
* node
,
140 const base::DictionaryValue
* strings
) {
141 switch (node
->GetType()) {
142 case base::Value::TYPE_LIST
: {
143 const base::ListValue
* list
= static_cast<const base::ListValue
*>(node
);
144 base::ListValue
* copy
= new base::ListValue
;
145 for (base::ListValue::const_iterator it
= list
->begin();
148 base::Value
* child_copy
= DeepCopyAndResolveStrings(*it
, strings
);
149 copy
->Append(child_copy
);
154 case base::Value::TYPE_DICTIONARY
: {
155 const base::DictionaryValue
* dict
=
156 static_cast<const base::DictionaryValue
*>(node
);
157 base::DictionaryValue
* copy
= new base::DictionaryValue
;
158 for (base::DictionaryValue::Iterator
it(*dict
);
161 base::Value
* child_copy
= DeepCopyAndResolveStrings(&it
.value(),
163 copy
->SetWithoutPathExpansion(it
.key(), child_copy
);
168 case base::Value::TYPE_STRING
: {
170 bool rv
= node
->GetAsString(&value
);
172 std::string actual_value
;
173 if (!strings
|| !strings
->GetString(value
, &actual_value
))
174 actual_value
= value
;
175 return new base::StringValue(actual_value
);
179 // For everything else, just make a copy.
180 return node
->DeepCopy();
184 void AppendQueryParameter(GURL
* url
,
185 const std::string
& param
,
186 const std::string
& value
) {
187 *url
= net::AppendQueryParameter(*url
, param
, value
);
192 NotificationPromo::NotificationPromo()
193 : prefs_(g_browser_process
->local_state()),
194 promo_type_(NO_PROMO
),
195 promo_payload_(new base::DictionaryValue()),
198 num_groups_(kDefaultGroupSize
),
209 new_notification_(false) {
213 NotificationPromo::~NotificationPromo() {}
215 void NotificationPromo::InitFromJson(const base::DictionaryValue
& json
,
216 PromoType promo_type
) {
217 promo_type_
= promo_type
;
218 const base::ListValue
* promo_list
= NULL
;
219 DVLOG(1) << "InitFromJson " << PromoTypeToString(promo_type_
);
220 if (!json
.GetList(PromoTypeToString(promo_type_
), &promo_list
))
223 // No support for multiple promos yet. Only consider the first one.
224 const base::DictionaryValue
* promo
= NULL
;
225 if (!promo_list
->GetDictionary(0, &promo
))
229 const base::ListValue
* date_list
= NULL
;
230 if (promo
->GetList("date", &date_list
)) {
231 const base::DictionaryValue
* date
;
232 if (date_list
->GetDictionary(0, &date
)) {
233 std::string time_str
;
235 if (date
->GetString("start", &time_str
) &&
236 base::Time::FromString(time_str
.c_str(), &time
)) {
237 start_
= time
.ToDoubleT();
238 DVLOG(1) << "start str=" << time_str
239 << ", start_="<< base::DoubleToString(start_
);
241 if (date
->GetString("end", &time_str
) &&
242 base::Time::FromString(time_str
.c_str(), &time
)) {
243 end_
= time
.ToDoubleT();
244 DVLOG(1) << "end str =" << time_str
245 << ", end_=" << base::DoubleToString(end_
);
251 const base::DictionaryValue
* grouping
= NULL
;
252 if (promo
->GetDictionary("grouping", &grouping
)) {
253 grouping
->GetInteger("buckets", &num_groups_
);
254 grouping
->GetInteger("segment", &initial_segment_
);
255 grouping
->GetInteger("increment", &increment_
);
256 grouping
->GetInteger("increment_frequency", &time_slice_
);
257 grouping
->GetInteger("increment_max", &max_group_
);
259 DVLOG(1) << "num_groups_ = " << num_groups_
260 << ", initial_segment_ = " << initial_segment_
261 << ", increment_ = " << increment_
262 << ", time_slice_ = " << time_slice_
263 << ", max_group_ = " << max_group_
;
267 const base::DictionaryValue
* strings
= NULL
;
268 promo
->GetDictionary("strings", &strings
);
271 const base::DictionaryValue
* payload
= NULL
;
272 if (promo
->GetDictionary("payload", &payload
)) {
273 base::Value
* ppcopy
= DeepCopyAndResolveStrings(payload
, strings
);
274 DCHECK(ppcopy
&& ppcopy
->IsType(base::Value::TYPE_DICTIONARY
));
275 promo_payload_
.reset(static_cast<base::DictionaryValue
*>(ppcopy
));
278 if (!promo_payload_
->GetString("promo_message_short", &promo_text_
) &&
280 // For compatibility with the legacy desktop version,
281 // if no |payload.promo_message_short| is specified,
282 // the first string in |strings| is used.
283 base::DictionaryValue::Iterator
iter(*strings
);
284 iter
.value().GetAsString(&promo_text_
);
286 DVLOG(1) << "promo_text_=" << promo_text_
;
288 promo
->GetInteger("max_views", &max_views_
);
289 DVLOG(1) << "max_views_ " << max_views_
;
291 promo
->GetInteger("max_seconds", &max_seconds_
);
292 DVLOG(1) << "max_seconds_ " << max_seconds_
;
294 CheckForNewNotification();
297 void NotificationPromo::CheckForNewNotification() {
298 NotificationPromo old_promo
;
299 old_promo
.InitFromPrefs(promo_type_
);
300 const double old_start
= old_promo
.start_
;
301 const double old_end
= old_promo
.end_
;
302 const std::string old_promo_text
= old_promo
.promo_text_
;
305 old_start
!= start_
|| old_end
!= end_
|| old_promo_text
!= promo_text_
;
306 if (new_notification_
)
310 void NotificationPromo::OnNewNotification() {
311 DVLOG(1) << "OnNewNotification";
312 // Create a new promo group.
313 group_
= base::RandInt(0, num_groups_
- 1);
318 void NotificationPromo::RegisterPrefs(PrefRegistrySimple
* registry
) {
319 registry
->RegisterDictionaryPref(kPrefPromoObject
);
323 void NotificationPromo::RegisterProfilePrefs(
324 user_prefs::PrefRegistrySyncable
* registry
) {
325 // TODO(dbeam): Registered only for migration. Remove in M28 when
326 // we're reasonably sure all prefs are gone.
327 // http://crbug.com/168887
328 registry
->RegisterDictionaryPref(
329 kPrefPromoObject
, user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF
);
333 void NotificationPromo::MigrateUserPrefs(PrefService
* user_prefs
) {
334 user_prefs
->ClearPref(kPrefPromoObject
);
337 void NotificationPromo::WritePrefs() {
338 base::DictionaryValue
* ntp_promo
= new base::DictionaryValue
;
339 ntp_promo
->SetString(kPrefPromoText
, promo_text_
);
340 ntp_promo
->Set(kPrefPromoPayload
, promo_payload_
->DeepCopy());
341 ntp_promo
->SetDouble(kPrefPromoStart
, start_
);
342 ntp_promo
->SetDouble(kPrefPromoEnd
, end_
);
344 ntp_promo
->SetInteger(kPrefPromoNumGroups
, num_groups_
);
345 ntp_promo
->SetInteger(kPrefPromoSegment
, initial_segment_
);
346 ntp_promo
->SetInteger(kPrefPromoIncrement
, increment_
);
347 ntp_promo
->SetInteger(kPrefPromoIncrementFrequency
, time_slice_
);
348 ntp_promo
->SetInteger(kPrefPromoIncrementMax
, max_group_
);
350 ntp_promo
->SetInteger(kPrefPromoMaxViews
, max_views_
);
351 ntp_promo
->SetInteger(kPrefPromoMaxSeconds
, max_seconds_
);
352 ntp_promo
->SetDouble(kPrefPromoFirstViewTime
, first_view_time_
);
354 ntp_promo
->SetInteger(kPrefPromoGroup
, group_
);
355 ntp_promo
->SetInteger(kPrefPromoViews
, views_
);
356 ntp_promo
->SetBoolean(kPrefPromoClosed
, closed_
);
358 base::ListValue
* promo_list
= new base::ListValue
;
359 promo_list
->Set(0, ntp_promo
); // Only support 1 promo for now.
361 base::DictionaryValue promo_dict
;
362 promo_dict
.MergeDictionary(prefs_
->GetDictionary(kPrefPromoObject
));
363 promo_dict
.Set(PromoTypeToString(promo_type_
), promo_list
);
364 prefs_
->Set(kPrefPromoObject
, promo_dict
);
365 DVLOG(1) << "WritePrefs " << promo_dict
;
368 void NotificationPromo::InitFromPrefs(PromoType promo_type
) {
369 promo_type_
= promo_type
;
370 const base::DictionaryValue
* promo_dict
=
371 prefs_
->GetDictionary(kPrefPromoObject
);
375 const base::ListValue
* promo_list
= NULL
;
376 promo_dict
->GetList(PromoTypeToString(promo_type_
), &promo_list
);
380 const base::DictionaryValue
* ntp_promo
= NULL
;
381 promo_list
->GetDictionary(0, &ntp_promo
);
385 ntp_promo
->GetString(kPrefPromoText
, &promo_text_
);
386 const base::DictionaryValue
* promo_payload
= NULL
;
387 if (ntp_promo
->GetDictionary(kPrefPromoPayload
, &promo_payload
))
388 promo_payload_
.reset(promo_payload
->DeepCopy());
390 ntp_promo
->GetDouble(kPrefPromoStart
, &start_
);
391 ntp_promo
->GetDouble(kPrefPromoEnd
, &end_
);
393 ntp_promo
->GetInteger(kPrefPromoNumGroups
, &num_groups_
);
394 ntp_promo
->GetInteger(kPrefPromoSegment
, &initial_segment_
);
395 ntp_promo
->GetInteger(kPrefPromoIncrement
, &increment_
);
396 ntp_promo
->GetInteger(kPrefPromoIncrementFrequency
, &time_slice_
);
397 ntp_promo
->GetInteger(kPrefPromoIncrementMax
, &max_group_
);
399 ntp_promo
->GetInteger(kPrefPromoMaxViews
, &max_views_
);
400 ntp_promo
->GetInteger(kPrefPromoMaxSeconds
, &max_seconds_
);
401 ntp_promo
->GetDouble(kPrefPromoFirstViewTime
, &first_view_time_
);
403 ntp_promo
->GetInteger(kPrefPromoGroup
, &group_
);
404 ntp_promo
->GetInteger(kPrefPromoViews
, &views_
);
405 ntp_promo
->GetBoolean(kPrefPromoClosed
, &closed_
);
408 bool NotificationPromo::CheckAppLauncher() const {
409 #if !defined(ENABLE_APP_LIST)
412 bool is_app_launcher_promo
= false;
413 if (!promo_payload_
->GetBoolean("is_app_launcher_promo",
414 &is_app_launcher_promo
))
416 return !is_app_launcher_promo
||
417 !prefs_
->GetBoolean(prefs::kAppLauncherIsEnabled
);
418 #endif // !defined(ENABLE_APP_LIST)
421 bool NotificationPromo::CanShow() const {
423 !promo_text_
.empty() &&
424 !ExceedsMaxGroup() &&
425 !ExceedsMaxViews() &&
426 !ExceedsMaxSeconds() &&
427 CheckAppLauncher() &&
428 base::Time::FromDoubleT(StartTimeForGroup()) < base::Time::Now() &&
429 base::Time::FromDoubleT(EndTime()) > base::Time::Now();
433 void NotificationPromo::HandleClosed(PromoType promo_type
) {
434 content::RecordAction(UserMetricsAction("NTPPromoClosed"));
435 NotificationPromo promo
;
436 promo
.InitFromPrefs(promo_type
);
437 if (!promo
.closed_
) {
438 promo
.closed_
= true;
444 bool NotificationPromo::HandleViewed(PromoType promo_type
) {
445 content::RecordAction(UserMetricsAction("NTPPromoShown"));
446 NotificationPromo promo
;
447 promo
.InitFromPrefs(promo_type
);
449 if (promo
.first_view_time_
== 0) {
450 promo
.first_view_time_
= base::Time::Now().ToDoubleT();
453 return promo
.ExceedsMaxViews() || promo
.ExceedsMaxSeconds();
456 bool NotificationPromo::ExceedsMaxGroup() const {
457 return (max_group_
== 0) ? false : group_
>= max_group_
;
460 bool NotificationPromo::ExceedsMaxViews() const {
461 return (max_views_
== 0) ? false : views_
>= max_views_
;
464 bool NotificationPromo::ExceedsMaxSeconds() const {
465 if (max_seconds_
== 0 || first_view_time_
== 0)
468 const base::Time last_view_time
= base::Time::FromDoubleT(first_view_time_
) +
469 base::TimeDelta::FromSeconds(max_seconds_
);
470 return last_view_time
< base::Time::Now();
474 GURL
NotificationPromo::PromoServerURL() {
475 GURL
url(promo_server_url
);
476 AppendQueryParameter(&url
, "dist", ChannelString());
477 AppendQueryParameter(&url
, "osname", PlatformString());
478 AppendQueryParameter(&url
, "branding", chrome::VersionInfo().Version());
479 AppendQueryParameter(&url
, "osver", base::SysInfo::OperatingSystemVersion());
480 DVLOG(1) << "PromoServerURL=" << url
.spec();
481 // Note that locale param is added by WebResourceService.
485 double NotificationPromo::StartTimeForGroup() const {
486 if (group_
< initial_segment_
)
489 std::ceil(static_cast<float>(group_
- initial_segment_
+ 1) / increment_
)
493 double NotificationPromo::EndTime() const {