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. 4 5 #include "chrome/browser/web_resource/notification_promo.h" 6 7 #include <cmath> 8 #include <vector> 9 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" 27 #include "ui/base/device_form_factor.h" 28 #include "url/gurl.h" 29 30 using content::UserMetricsAction; 31 32 namespace { 33 34 const int kDefaultGroupSize = 100; 35 36 const char promo_server_url[] = "https://clients3.google.com/crsignal/client"; 37 38 // The name of the preference that stores the promotion object. 39 const char kPrefPromoObject[] = "promo"; 40 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 kPrefPromoGroup[] = "group"; 53 const char kPrefPromoViews[] = "views"; 54 const char kPrefPromoClosed[] = "closed"; 55 56 // Returns a string suitable for the Promo Server URL 'osname' value. 57 std::string PlatformString() { 58 #if defined(OS_WIN) 59 return "win"; 60 #elif defined(OS_ANDROID) 61 ui::DeviceFormFactor form_factor = ui::GetDeviceFormFactor(); 62 return std::string("android-") + 63 (form_factor == ui::DEVICE_FORM_FACTOR_TABLET ? "tablet" : "phone"); 64 #elif defined(OS_IOS) 65 ui::DeviceFormFactor form_factor = ui::GetDeviceFormFactor(); 66 return std::string("ios-") + 67 (form_factor == ui::DEVICE_FORM_FACTOR_TABLET ? "tablet" : "phone"); 68 #elif defined(OS_MACOSX) 69 return "mac"; 70 #elif defined(OS_CHROMEOS) 71 return "chromeos"; 72 #elif defined(OS_LINUX) 73 return "linux"; 74 #else 75 return "none"; 76 #endif 77 } 78 79 // Returns a string suitable for the Promo Server URL 'dist' value. 80 const char* ChannelString() { 81 #if defined (OS_WIN) 82 // GetChannel hits the registry on Windows. See http://crbug.com/70898. 83 // TODO(achuith): Move NotificationPromo::PromoServerURL to the blocking pool. 84 base::ThreadRestrictions::ScopedAllowIO allow_io; 85 #endif 86 const chrome::VersionInfo::Channel channel = 87 chrome::VersionInfo::GetChannel(); 88 switch (channel) { 89 case chrome::VersionInfo::CHANNEL_CANARY: 90 return "canary"; 91 case chrome::VersionInfo::CHANNEL_DEV: 92 return "dev"; 93 case chrome::VersionInfo::CHANNEL_BETA: 94 return "beta"; 95 case chrome::VersionInfo::CHANNEL_STABLE: 96 return "stable"; 97 default: 98 return "none"; 99 } 100 } 101 102 struct PromoMapEntry { 103 NotificationPromo::PromoType promo_type; 104 const char* promo_type_str; 105 }; 106 107 const PromoMapEntry kPromoMap[] = { 108 { NotificationPromo::NO_PROMO, "" }, 109 { NotificationPromo::NTP_NOTIFICATION_PROMO, "ntp_notification_promo" }, 110 { NotificationPromo::NTP_BUBBLE_PROMO, "ntp_bubble_promo" }, 111 { NotificationPromo::MOBILE_NTP_SYNC_PROMO, "mobile_ntp_sync_promo" }, 112 }; 113 114 // Convert PromoType to appropriate string. 115 const char* PromoTypeToString(NotificationPromo::PromoType promo_type) { 116 for (size_t i = 0; i < arraysize(kPromoMap); ++i) { 117 if (kPromoMap[i].promo_type == promo_type) 118 return kPromoMap[i].promo_type_str; 119 } 120 NOTREACHED(); 121 return ""; 122 } 123 124 // Deep-copies a node, replacing any "value" that is a key 125 // into "strings" dictionary with its value from "strings". 126 // E.g. for 127 // {promo_action_args:['MSG_SHORT']} + strings:{MSG_SHORT:'yes'} 128 // it will return 129 // {promo_action_args:['yes']} 130 // |node| - a value to be deep copied and resolved. 131 // |strings| - a dictionary of strings to be used for resolution. 132 // Returns a _new_ object that is a deep copy with replacements. 133 // TODO(aruslan): http://crbug.com/144320 Consider moving it to values.cc/h. 134 base::Value* DeepCopyAndResolveStrings( 135 const base::Value* node, 136 const base::DictionaryValue* strings) { 137 switch (node->GetType()) { 138 case base::Value::TYPE_LIST: { 139 const base::ListValue* list = static_cast<const base::ListValue*>(node); 140 base::ListValue* copy = new base::ListValue; 141 for (base::ListValue::const_iterator it = list->begin(); 142 it != list->end(); 143 ++it) { 144 base::Value* child_copy = DeepCopyAndResolveStrings(*it, strings); 145 copy->Append(child_copy); 146 } 147 return copy; 148 } 149 150 case Value::TYPE_DICTIONARY: { 151 const base::DictionaryValue* dict = 152 static_cast<const base::DictionaryValue*>(node); 153 base::DictionaryValue* copy = new base::DictionaryValue; 154 for (base::DictionaryValue::Iterator it(*dict); 155 !it.IsAtEnd(); 156 it.Advance()) { 157 base::Value* child_copy = DeepCopyAndResolveStrings(&it.value(), 158 strings); 159 copy->SetWithoutPathExpansion(it.key(), child_copy); 160 } 161 return copy; 162 } 163 164 case Value::TYPE_STRING: { 165 std::string value; 166 bool rv = node->GetAsString(&value); 167 DCHECK(rv); 168 std::string actual_value; 169 if (!strings || !strings->GetString(value, &actual_value)) 170 actual_value = value; 171 return new base::StringValue(actual_value); 172 } 173 174 default: 175 // For everything else, just make a copy. 176 return node->DeepCopy(); 177 } 178 } 179 180 void AppendQueryParameter(GURL* url, 181 const std::string& param, 182 const std::string& value) { 183 *url = net::AppendQueryParameter(*url, param, value); 184 } 185 186 } // namespace 187 188 NotificationPromo::NotificationPromo() 189 : prefs_(g_browser_process->local_state()), 190 promo_type_(NO_PROMO), 191 promo_payload_(new base::DictionaryValue()), 192 start_(0.0), 193 end_(0.0), 194 num_groups_(kDefaultGroupSize), 195 initial_segment_(0), 196 increment_(1), 197 time_slice_(0), 198 max_group_(0), 199 max_views_(0), 200 group_(0), 201 views_(0), 202 closed_(false), 203 new_notification_(false) { 204 DCHECK(prefs_); 205 } 206 207 NotificationPromo::~NotificationPromo() {} 208 209 void NotificationPromo::InitFromJson(const DictionaryValue& json, 210 PromoType promo_type) { 211 promo_type_ = promo_type; 212 const ListValue* promo_list = NULL; 213 DVLOG(1) << "InitFromJson " << PromoTypeToString(promo_type_); 214 if (!json.GetList(PromoTypeToString(promo_type_), &promo_list)) 215 return; 216 217 // No support for multiple promos yet. Only consider the first one. 218 const DictionaryValue* promo = NULL; 219 if (!promo_list->GetDictionary(0, &promo)) 220 return; 221 222 // Date. 223 const ListValue* date_list = NULL; 224 if (promo->GetList("date", &date_list)) { 225 const DictionaryValue* date; 226 if (date_list->GetDictionary(0, &date)) { 227 std::string time_str; 228 base::Time time; 229 if (date->GetString("start", &time_str) && 230 base::Time::FromString(time_str.c_str(), &time)) { 231 start_ = time.ToDoubleT(); 232 DVLOG(1) << "start str=" << time_str 233 << ", start_="<< base::DoubleToString(start_); 234 } 235 if (date->GetString("end", &time_str) && 236 base::Time::FromString(time_str.c_str(), &time)) { 237 end_ = time.ToDoubleT(); 238 DVLOG(1) << "end str =" << time_str 239 << ", end_=" << base::DoubleToString(end_); 240 } 241 } 242 } 243 244 // Grouping. 245 const DictionaryValue* grouping = NULL; 246 if (promo->GetDictionary("grouping", &grouping)) { 247 grouping->GetInteger("buckets", &num_groups_); 248 grouping->GetInteger("segment", &initial_segment_); 249 grouping->GetInteger("increment", &increment_); 250 grouping->GetInteger("increment_frequency", &time_slice_); 251 grouping->GetInteger("increment_max", &max_group_); 252 253 DVLOG(1) << "num_groups_ = " << num_groups_ 254 << ", initial_segment_ = " << initial_segment_ 255 << ", increment_ = " << increment_ 256 << ", time_slice_ = " << time_slice_ 257 << ", max_group_ = " << max_group_; 258 } 259 260 // Strings. 261 const DictionaryValue* strings = NULL; 262 promo->GetDictionary("strings", &strings); 263 264 // Payload. 265 const DictionaryValue* payload = NULL; 266 if (promo->GetDictionary("payload", &payload)) { 267 base::Value* ppcopy = DeepCopyAndResolveStrings(payload, strings); 268 DCHECK(ppcopy && ppcopy->IsType(base::Value::TYPE_DICTIONARY)); 269 promo_payload_.reset(static_cast<base::DictionaryValue*>(ppcopy)); 270 } 271 272 if (!promo_payload_->GetString("promo_message_short", &promo_text_) && 273 strings) { 274 // For compatibility with the legacy desktop version, 275 // if no |payload.promo_message_short| is specified, 276 // the first string in |strings| is used. 277 DictionaryValue::Iterator iter(*strings); 278 iter.value().GetAsString(&promo_text_); 279 } 280 DVLOG(1) << "promo_text_=" << promo_text_; 281 282 promo->GetInteger("max_views", &max_views_); 283 DVLOG(1) << "max_views_ " << max_views_; 284 285 CheckForNewNotification(); 286 } 287 288 void NotificationPromo::CheckForNewNotification() { 289 NotificationPromo old_promo; 290 old_promo.InitFromPrefs(promo_type_); 291 const double old_start = old_promo.start_; 292 const double old_end = old_promo.end_; 293 const std::string old_promo_text = old_promo.promo_text_; 294 295 new_notification_ = 296 old_start != start_ || old_end != end_ || old_promo_text != promo_text_; 297 if (new_notification_) 298 OnNewNotification(); 299 } 300 301 void NotificationPromo::OnNewNotification() { 302 DVLOG(1) << "OnNewNotification"; 303 // Create a new promo group. 304 group_ = base::RandInt(0, num_groups_ - 1); 305 WritePrefs(); 306 } 307 308 // static 309 void NotificationPromo::RegisterPrefs(PrefRegistrySimple* registry) { 310 registry->RegisterDictionaryPref(kPrefPromoObject); 311 } 312 313 // static 314 void NotificationPromo::RegisterProfilePrefs( 315 user_prefs::PrefRegistrySyncable* registry) { 316 // TODO(dbeam): Registered only for migration. Remove in M28 when 317 // we're reasonably sure all prefs are gone. 318 // http://crbug.com/168887 319 registry->RegisterDictionaryPref( 320 kPrefPromoObject, user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF); 321 } 322 323 // static 324 void NotificationPromo::MigrateUserPrefs(PrefService* user_prefs) { 325 user_prefs->ClearPref(kPrefPromoObject); 326 } 327 328 void NotificationPromo::WritePrefs() { 329 base::DictionaryValue* ntp_promo = new base::DictionaryValue; 330 ntp_promo->SetString(kPrefPromoText, promo_text_); 331 ntp_promo->Set(kPrefPromoPayload, promo_payload_->DeepCopy()); 332 ntp_promo->SetDouble(kPrefPromoStart, start_); 333 ntp_promo->SetDouble(kPrefPromoEnd, end_); 334 335 ntp_promo->SetInteger(kPrefPromoNumGroups, num_groups_); 336 ntp_promo->SetInteger(kPrefPromoSegment, initial_segment_); 337 ntp_promo->SetInteger(kPrefPromoIncrement, increment_); 338 ntp_promo->SetInteger(kPrefPromoIncrementFrequency, time_slice_); 339 ntp_promo->SetInteger(kPrefPromoIncrementMax, max_group_); 340 341 ntp_promo->SetInteger(kPrefPromoMaxViews, max_views_); 342 343 ntp_promo->SetInteger(kPrefPromoGroup, group_); 344 ntp_promo->SetInteger(kPrefPromoViews, views_); 345 ntp_promo->SetBoolean(kPrefPromoClosed, closed_); 346 347 base::ListValue* promo_list = new base::ListValue; 348 promo_list->Set(0, ntp_promo); // Only support 1 promo for now. 349 350 base::DictionaryValue promo_dict; 351 promo_dict.MergeDictionary(prefs_->GetDictionary(kPrefPromoObject)); 352 promo_dict.Set(PromoTypeToString(promo_type_), promo_list); 353 prefs_->Set(kPrefPromoObject, promo_dict); 354 DVLOG(1) << "WritePrefs " << promo_dict; 355 } 356 357 void NotificationPromo::InitFromPrefs(PromoType promo_type) { 358 promo_type_ = promo_type; 359 const base::DictionaryValue* promo_dict = 360 prefs_->GetDictionary(kPrefPromoObject); 361 if (!promo_dict) 362 return; 363 364 const base::ListValue* promo_list = NULL; 365 promo_dict->GetList(PromoTypeToString(promo_type_), &promo_list); 366 if (!promo_list) 367 return; 368 369 const base::DictionaryValue* ntp_promo = NULL; 370 promo_list->GetDictionary(0, &ntp_promo); 371 if (!ntp_promo) 372 return; 373 374 ntp_promo->GetString(kPrefPromoText, &promo_text_); 375 const base::DictionaryValue* promo_payload = NULL; 376 if (ntp_promo->GetDictionary(kPrefPromoPayload, &promo_payload)) 377 promo_payload_.reset(promo_payload->DeepCopy()); 378 379 ntp_promo->GetDouble(kPrefPromoStart, &start_); 380 ntp_promo->GetDouble(kPrefPromoEnd, &end_); 381 382 ntp_promo->GetInteger(kPrefPromoNumGroups, &num_groups_); 383 ntp_promo->GetInteger(kPrefPromoSegment, &initial_segment_); 384 ntp_promo->GetInteger(kPrefPromoIncrement, &increment_); 385 ntp_promo->GetInteger(kPrefPromoIncrementFrequency, &time_slice_); 386 ntp_promo->GetInteger(kPrefPromoIncrementMax, &max_group_); 387 388 ntp_promo->GetInteger(kPrefPromoMaxViews, &max_views_); 389 390 ntp_promo->GetInteger(kPrefPromoGroup, &group_); 391 ntp_promo->GetInteger(kPrefPromoViews, &views_); 392 ntp_promo->GetBoolean(kPrefPromoClosed, &closed_); 393 } 394 395 bool NotificationPromo::CheckAppLauncher() const { 396 #if !defined(ENABLE_APP_LIST) 397 return true; 398 #else 399 bool is_app_launcher_promo = false; 400 if (!promo_payload_->GetBoolean("is_app_launcher_promo", 401 &is_app_launcher_promo)) 402 return true; 403 return !is_app_launcher_promo || 404 !prefs_->GetBoolean(prefs::kAppLauncherIsEnabled); 405 #endif // !defined(ENABLE_APP_LIST) 406 } 407 408 bool NotificationPromo::CanShow() const { 409 return !closed_ && 410 !promo_text_.empty() && 411 !ExceedsMaxGroup() && 412 !ExceedsMaxViews() && 413 CheckAppLauncher() && 414 base::Time::FromDoubleT(StartTimeForGroup()) < base::Time::Now() && 415 base::Time::FromDoubleT(EndTime()) > base::Time::Now(); 416 } 417 418 // static 419 void NotificationPromo::HandleClosed(PromoType promo_type) { 420 content::RecordAction(UserMetricsAction("NTPPromoClosed")); 421 NotificationPromo promo; 422 promo.InitFromPrefs(promo_type); 423 if (!promo.closed_) { 424 promo.closed_ = true; 425 promo.WritePrefs(); 426 } 427 } 428 429 // static 430 bool NotificationPromo::HandleViewed(PromoType promo_type) { 431 content::RecordAction(UserMetricsAction("NTPPromoShown")); 432 NotificationPromo promo; 433 promo.InitFromPrefs(promo_type); 434 ++promo.views_; 435 promo.WritePrefs(); 436 return promo.ExceedsMaxViews(); 437 } 438 439 bool NotificationPromo::ExceedsMaxGroup() const { 440 return (max_group_ == 0) ? false : group_ >= max_group_; 441 } 442 443 bool NotificationPromo::ExceedsMaxViews() const { 444 return (max_views_ == 0) ? false : views_ >= max_views_; 445 } 446 447 // static 448 GURL NotificationPromo::PromoServerURL() { 449 GURL url(promo_server_url); 450 AppendQueryParameter(&url, "dist", ChannelString()); 451 AppendQueryParameter(&url, "osname", PlatformString()); 452 AppendQueryParameter(&url, "branding", chrome::VersionInfo().Version()); 453 AppendQueryParameter(&url, "osver", base::SysInfo::OperatingSystemVersion()); 454 DVLOG(1) << "PromoServerURL=" << url.spec(); 455 // Note that locale param is added by WebResourceService. 456 return url; 457 } 458 459 double NotificationPromo::StartTimeForGroup() const { 460 if (group_ < initial_segment_) 461 return start_; 462 return start_ + 463 std::ceil(static_cast<float>(group_ - initial_segment_ + 1) / increment_) 464 * time_slice_; 465 } 466 467 double NotificationPromo::EndTime() const { 468 return end_; 469 } 470