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/user_prefs/pref_registry_syncable.h"
25 #include "content/public/browser/user_metrics.h"
26 #include "net/base/url_util.h"
29 #if defined(OS_ANDROID)
30 #include "base/command_line.h"
31 #include "chrome/common/chrome_switches.h"
32 #endif // defined(OS_ANDROID)
34 using content::UserMetricsAction;
38 const int kDefaultGroupSize = 100;
40 const char promo_server_url[] = "https://clients3.google.com/crsignal/client";
42 // The name of the preference that stores the promotion object.
43 const char kPrefPromoObject[] = "promo";
45 // Keys in the kPrefPromoObject dictionary; used only here.
46 const char kPrefPromoText[] = "text";
47 const char kPrefPromoPayload[] = "payload";
48 const char kPrefPromoStart[] = "start";
49 const char kPrefPromoEnd[] = "end";
50 const char kPrefPromoNumGroups[] = "num_groups";
51 const char kPrefPromoSegment[] = "segment";
52 const char kPrefPromoIncrement[] = "increment";
53 const char kPrefPromoIncrementFrequency[] = "increment_frequency";
54 const char kPrefPromoIncrementMax[] = "increment_max";
55 const char kPrefPromoMaxViews[] = "max_views";
56 const char kPrefPromoGroup[] = "group";
57 const char kPrefPromoViews[] = "views";
58 const char kPrefPromoClosed[] = "closed";
60 // Returns a string suitable for the Promo Server URL 'osname' value.
61 std::string PlatformString() {
65 // TODO(noyau): add iOS-specific implementation
66 const bool isTablet = false;
67 return std::string("ios-") + (isTablet ? "tablet" : "phone");
68 #elif defined(OS_MACOSX)
70 #elif defined(OS_CHROMEOS)
72 #elif defined(OS_LINUX)
74 #elif defined(OS_ANDROID)
76 CommandLine::ForCurrentProcess()->HasSwitch(switches::kTabletUI);
77 return std::string("android-") + (isTablet ? "tablet" : "phone");
83 // Returns a string suitable for the Promo Server URL 'dist' value.
84 const char* ChannelString() {
86 // GetChannel hits the registry on Windows. See http://crbug.com/70898.
87 // TODO(achuith): Move NotificationPromo::PromoServerURL to the blocking pool.
88 base::ThreadRestrictions::ScopedAllowIO allow_io;
90 const chrome::VersionInfo::Channel channel =
91 chrome::VersionInfo::GetChannel();
93 case chrome::VersionInfo::CHANNEL_CANARY:
95 case chrome::VersionInfo::CHANNEL_DEV:
97 case chrome::VersionInfo::CHANNEL_BETA:
99 case chrome::VersionInfo::CHANNEL_STABLE:
106 struct PromoMapEntry {
107 NotificationPromo::PromoType promo_type;
108 const char* promo_type_str;
111 const PromoMapEntry kPromoMap[] = {
112 { NotificationPromo::NO_PROMO, "" },
113 { NotificationPromo::NTP_NOTIFICATION_PROMO, "ntp_notification_promo" },
114 { NotificationPromo::NTP_BUBBLE_PROMO, "ntp_bubble_promo" },
115 { NotificationPromo::MOBILE_NTP_SYNC_PROMO, "mobile_ntp_sync_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 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 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),
207 new_notification_(false) {
211 NotificationPromo::~NotificationPromo() {}
213 void NotificationPromo::InitFromJson(const DictionaryValue& json,
214 PromoType promo_type) {
215 promo_type_ = promo_type;
216 const ListValue* promo_list = NULL;
217 DVLOG(1) << "InitFromJson " << PromoTypeToString(promo_type_);
218 if (!json.GetList(PromoTypeToString(promo_type_), &promo_list))
221 // No support for multiple promos yet. Only consider the first one.
222 const DictionaryValue* promo = NULL;
223 if (!promo_list->GetDictionary(0, &promo))
227 const ListValue* date_list = NULL;
228 if (promo->GetList("date", &date_list)) {
229 const DictionaryValue* date;
230 if (date_list->GetDictionary(0, &date)) {
231 std::string time_str;
233 if (date->GetString("start", &time_str) &&
234 base::Time::FromString(time_str.c_str(), &time)) {
235 start_ = time.ToDoubleT();
236 DVLOG(1) << "start str=" << time_str
237 << ", start_="<< base::DoubleToString(start_);
239 if (date->GetString("end", &time_str) &&
240 base::Time::FromString(time_str.c_str(), &time)) {
241 end_ = time.ToDoubleT();
242 DVLOG(1) << "end str =" << time_str
243 << ", end_=" << base::DoubleToString(end_);
249 const DictionaryValue* grouping = NULL;
250 if (promo->GetDictionary("grouping", &grouping)) {
251 grouping->GetInteger("buckets", &num_groups_);
252 grouping->GetInteger("segment", &initial_segment_);
253 grouping->GetInteger("increment", &increment_);
254 grouping->GetInteger("increment_frequency", &time_slice_);
255 grouping->GetInteger("increment_max", &max_group_);
257 DVLOG(1) << "num_groups_ = " << num_groups_
258 << ", initial_segment_ = " << initial_segment_
259 << ", increment_ = " << increment_
260 << ", time_slice_ = " << time_slice_
261 << ", max_group_ = " << max_group_;
265 const DictionaryValue* strings = NULL;
266 promo->GetDictionary("strings", &strings);
269 const DictionaryValue* payload = NULL;
270 if (promo->GetDictionary("payload", &payload)) {
271 base::Value* ppcopy = DeepCopyAndResolveStrings(payload, strings);
272 DCHECK(ppcopy && ppcopy->IsType(base::Value::TYPE_DICTIONARY));
273 promo_payload_.reset(static_cast<base::DictionaryValue*>(ppcopy));
276 if (!promo_payload_->GetString("promo_message_short", &promo_text_) &&
278 // For compatibility with the legacy desktop version,
279 // if no |payload.promo_message_short| is specified,
280 // the first string in |strings| is used.
281 DictionaryValue::Iterator iter(*strings);
282 iter.value().GetAsString(&promo_text_);
284 DVLOG(1) << "promo_text_=" << promo_text_;
286 promo->GetInteger("max_views", &max_views_);
287 DVLOG(1) << "max_views_ " << max_views_;
289 CheckForNewNotification();
292 void NotificationPromo::CheckForNewNotification() {
293 NotificationPromo old_promo;
294 old_promo.InitFromPrefs(promo_type_);
295 const double old_start = old_promo.start_;
296 const double old_end = old_promo.end_;
297 const std::string old_promo_text = old_promo.promo_text_;
300 old_start != start_ || old_end != end_ || old_promo_text != promo_text_;
301 if (new_notification_)
305 void NotificationPromo::OnNewNotification() {
306 DVLOG(1) << "OnNewNotification";
307 // Create a new promo group.
308 group_ = base::RandInt(0, num_groups_ - 1);
313 void NotificationPromo::RegisterPrefs(PrefRegistrySimple* registry) {
314 registry->RegisterDictionaryPref(kPrefPromoObject);
318 void NotificationPromo::RegisterProfilePrefs(
319 user_prefs::PrefRegistrySyncable* registry) {
320 // TODO(dbeam): Registered only for migration. Remove in M28 when
321 // we're reasonably sure all prefs are gone.
322 // http://crbug.com/168887
323 registry->RegisterDictionaryPref(
324 kPrefPromoObject, user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF);
328 void NotificationPromo::MigrateUserPrefs(PrefService* user_prefs) {
329 user_prefs->ClearPref(kPrefPromoObject);
332 void NotificationPromo::WritePrefs() {
333 base::DictionaryValue* ntp_promo = new base::DictionaryValue;
334 ntp_promo->SetString(kPrefPromoText, promo_text_);
335 ntp_promo->Set(kPrefPromoPayload, promo_payload_->DeepCopy());
336 ntp_promo->SetDouble(kPrefPromoStart, start_);
337 ntp_promo->SetDouble(kPrefPromoEnd, end_);
339 ntp_promo->SetInteger(kPrefPromoNumGroups, num_groups_);
340 ntp_promo->SetInteger(kPrefPromoSegment, initial_segment_);
341 ntp_promo->SetInteger(kPrefPromoIncrement, increment_);
342 ntp_promo->SetInteger(kPrefPromoIncrementFrequency, time_slice_);
343 ntp_promo->SetInteger(kPrefPromoIncrementMax, max_group_);
345 ntp_promo->SetInteger(kPrefPromoMaxViews, max_views_);
347 ntp_promo->SetInteger(kPrefPromoGroup, group_);
348 ntp_promo->SetInteger(kPrefPromoViews, views_);
349 ntp_promo->SetBoolean(kPrefPromoClosed, closed_);
351 base::ListValue* promo_list = new base::ListValue;
352 promo_list->Set(0, ntp_promo); // Only support 1 promo for now.
354 base::DictionaryValue promo_dict;
355 promo_dict.MergeDictionary(prefs_->GetDictionary(kPrefPromoObject));
356 promo_dict.Set(PromoTypeToString(promo_type_), promo_list);
357 prefs_->Set(kPrefPromoObject, promo_dict);
358 DVLOG(1) << "WritePrefs " << promo_dict;
361 void NotificationPromo::InitFromPrefs(PromoType promo_type) {
362 promo_type_ = promo_type;
363 const base::DictionaryValue* promo_dict =
364 prefs_->GetDictionary(kPrefPromoObject);
368 const base::ListValue* promo_list = NULL;
369 promo_dict->GetList(PromoTypeToString(promo_type_), &promo_list);
373 const base::DictionaryValue* ntp_promo = NULL;
374 promo_list->GetDictionary(0, &ntp_promo);
378 ntp_promo->GetString(kPrefPromoText, &promo_text_);
379 const base::DictionaryValue* promo_payload = NULL;
380 if (ntp_promo->GetDictionary(kPrefPromoPayload, &promo_payload))
381 promo_payload_.reset(promo_payload->DeepCopy());
383 ntp_promo->GetDouble(kPrefPromoStart, &start_);
384 ntp_promo->GetDouble(kPrefPromoEnd, &end_);
386 ntp_promo->GetInteger(kPrefPromoNumGroups, &num_groups_);
387 ntp_promo->GetInteger(kPrefPromoSegment, &initial_segment_);
388 ntp_promo->GetInteger(kPrefPromoIncrement, &increment_);
389 ntp_promo->GetInteger(kPrefPromoIncrementFrequency, &time_slice_);
390 ntp_promo->GetInteger(kPrefPromoIncrementMax, &max_group_);
392 ntp_promo->GetInteger(kPrefPromoMaxViews, &max_views_);
394 ntp_promo->GetInteger(kPrefPromoGroup, &group_);
395 ntp_promo->GetInteger(kPrefPromoViews, &views_);
396 ntp_promo->GetBoolean(kPrefPromoClosed, &closed_);
399 bool NotificationPromo::CheckAppLauncher() const {
400 #if !defined(ENABLE_APP_LIST)
403 bool is_app_launcher_promo = false;
404 if (!promo_payload_->GetBoolean("is_app_launcher_promo",
405 &is_app_launcher_promo))
407 return !is_app_launcher_promo ||
408 !prefs_->GetBoolean(prefs::kAppLauncherIsEnabled);
409 #endif // !defined(ENABLE_APP_LIST)
412 bool NotificationPromo::CanShow() const {
414 !promo_text_.empty() &&
415 !ExceedsMaxGroup() &&
416 !ExceedsMaxViews() &&
417 CheckAppLauncher() &&
418 base::Time::FromDoubleT(StartTimeForGroup()) < base::Time::Now() &&
419 base::Time::FromDoubleT(EndTime()) > base::Time::Now();
423 void NotificationPromo::HandleClosed(PromoType promo_type) {
424 content::RecordAction(UserMetricsAction("NTPPromoClosed"));
425 NotificationPromo promo;
426 promo.InitFromPrefs(promo_type);
427 if (!promo.closed_) {
428 promo.closed_ = true;
434 bool NotificationPromo::HandleViewed(PromoType promo_type) {
435 content::RecordAction(UserMetricsAction("NTPPromoShown"));
436 NotificationPromo promo;
437 promo.InitFromPrefs(promo_type);
440 return promo.ExceedsMaxViews();
443 bool NotificationPromo::ExceedsMaxGroup() const {
444 return (max_group_ == 0) ? false : group_ >= max_group_;
447 bool NotificationPromo::ExceedsMaxViews() const {
448 return (max_views_ == 0) ? false : views_ >= max_views_;
452 GURL NotificationPromo::PromoServerURL() {
453 GURL url(promo_server_url);
454 AppendQueryParameter(&url, "dist", ChannelString());
455 AppendQueryParameter(&url, "osname", PlatformString());
456 AppendQueryParameter(&url, "branding", chrome::VersionInfo().Version());
457 AppendQueryParameter(&url, "osver", base::SysInfo::OperatingSystemVersion());
458 DVLOG(1) << "PromoServerURL=" << url.spec();
459 // Note that locale param is added by WebResourceService.
463 double NotificationPromo::StartTimeForGroup() const {
464 if (group_ < initial_segment_)
467 std::ceil(static_cast<float>(group_ - initial_segment_ + 1) / increment_)
471 double NotificationPromo::EndTime() const {