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/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" 28 #include "url/gurl.h" 29 30 using base::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 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"; 57 58 // Returns a string suitable for the Promo Server URL 'osname' value. 59 std::string PlatformString() { 60 #if defined(OS_WIN) 61 return "win"; 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"); 66 #elif defined(OS_IOS) 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) 71 return "mac"; 72 #elif defined(OS_CHROMEOS) 73 return "chromeos"; 74 #elif defined(OS_LINUX) 75 return "linux"; 76 #else 77 return "none"; 78 #endif 79 } 80 81 // Returns a string suitable for the Promo Server URL 'dist' value. 82 const char* ChannelString() { 83 #if defined (OS_WIN) 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; 87 #endif 88 const chrome::VersionInfo::Channel channel = 89 chrome::VersionInfo::GetChannel(); 90 switch (channel) { 91 case chrome::VersionInfo::CHANNEL_CANARY: 92 return "canary"; 93 case chrome::VersionInfo::CHANNEL_DEV: 94 return "dev"; 95 case chrome::VersionInfo::CHANNEL_BETA: 96 return "beta"; 97 case chrome::VersionInfo::CHANNEL_STABLE: 98 return "stable"; 99 default: 100 return "none"; 101 } 102 } 103 104 struct PromoMapEntry { 105 NotificationPromo::PromoType promo_type; 106 const char* promo_type_str; 107 }; 108 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" }, 116 }; 117 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; 123 } 124 NOTREACHED(); 125 return ""; 126 } 127 128 // Deep-copies a node, replacing any "value" that is a key 129 // into "strings" dictionary with its value from "strings". 130 // E.g. for 131 // {promo_action_args:['MSG_SHORT']} + strings:{MSG_SHORT:'yes'} 132 // it will return 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(); 146 it != list->end(); 147 ++it) { 148 base::Value* child_copy = DeepCopyAndResolveStrings(*it, strings); 149 copy->Append(child_copy); 150 } 151 return copy; 152 } 153 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); 159 !it.IsAtEnd(); 160 it.Advance()) { 161 base::Value* child_copy = DeepCopyAndResolveStrings(&it.value(), 162 strings); 163 copy->SetWithoutPathExpansion(it.key(), child_copy); 164 } 165 return copy; 166 } 167 168 case base::Value::TYPE_STRING: { 169 std::string value; 170 bool rv = node->GetAsString(&value); 171 DCHECK(rv); 172 std::string actual_value; 173 if (!strings || !strings->GetString(value, &actual_value)) 174 actual_value = value; 175 return new base::StringValue(actual_value); 176 } 177 178 default: 179 // For everything else, just make a copy. 180 return node->DeepCopy(); 181 } 182 } 183 184 void AppendQueryParameter(GURL* url, 185 const std::string& param, 186 const std::string& value) { 187 *url = net::AppendQueryParameter(*url, param, value); 188 } 189 190 } // namespace 191 192 NotificationPromo::NotificationPromo() 193 : prefs_(g_browser_process->local_state()), 194 promo_type_(NO_PROMO), 195 promo_payload_(new base::DictionaryValue()), 196 start_(0.0), 197 end_(0.0), 198 num_groups_(kDefaultGroupSize), 199 initial_segment_(0), 200 increment_(1), 201 time_slice_(0), 202 max_group_(0), 203 max_views_(0), 204 max_seconds_(0), 205 first_view_time_(0), 206 group_(0), 207 views_(0), 208 closed_(false), 209 new_notification_(false) { 210 DCHECK(prefs_); 211 } 212 213 NotificationPromo::~NotificationPromo() {} 214 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)) 221 return; 222 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)) 226 return; 227 228 // Date. 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; 234 base::Time time; 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_); 240 } 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_); 246 } 247 } 248 } 249 250 // Grouping. 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_); 258 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_; 264 } 265 266 // Strings. 267 const base::DictionaryValue* strings = NULL; 268 promo->GetDictionary("strings", &strings); 269 270 // Payload. 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)); 276 } 277 278 if (!promo_payload_->GetString("promo_message_short", &promo_text_) && 279 strings) { 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_); 285 } 286 DVLOG(1) << "promo_text_=" << promo_text_; 287 288 promo->GetInteger("max_views", &max_views_); 289 DVLOG(1) << "max_views_ " << max_views_; 290 291 promo->GetInteger("max_seconds", &max_seconds_); 292 DVLOG(1) << "max_seconds_ " << max_seconds_; 293 294 CheckForNewNotification(); 295 } 296 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_; 303 304 new_notification_ = 305 old_start != start_ || old_end != end_ || old_promo_text != promo_text_; 306 if (new_notification_) 307 OnNewNotification(); 308 } 309 310 void NotificationPromo::OnNewNotification() { 311 DVLOG(1) << "OnNewNotification"; 312 // Create a new promo group. 313 group_ = base::RandInt(0, num_groups_ - 1); 314 WritePrefs(); 315 } 316 317 // static 318 void NotificationPromo::RegisterPrefs(PrefRegistrySimple* registry) { 319 registry->RegisterDictionaryPref(kPrefPromoObject); 320 } 321 322 // static 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); 330 } 331 332 // static 333 void NotificationPromo::MigrateUserPrefs(PrefService* user_prefs) { 334 user_prefs->ClearPref(kPrefPromoObject); 335 } 336 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_); 343 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_); 349 350 ntp_promo->SetInteger(kPrefPromoMaxViews, max_views_); 351 ntp_promo->SetInteger(kPrefPromoMaxSeconds, max_seconds_); 352 ntp_promo->SetDouble(kPrefPromoFirstViewTime, first_view_time_); 353 354 ntp_promo->SetInteger(kPrefPromoGroup, group_); 355 ntp_promo->SetInteger(kPrefPromoViews, views_); 356 ntp_promo->SetBoolean(kPrefPromoClosed, closed_); 357 358 base::ListValue* promo_list = new base::ListValue; 359 promo_list->Set(0, ntp_promo); // Only support 1 promo for now. 360 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; 366 } 367 368 void NotificationPromo::InitFromPrefs(PromoType promo_type) { 369 promo_type_ = promo_type; 370 const base::DictionaryValue* promo_dict = 371 prefs_->GetDictionary(kPrefPromoObject); 372 if (!promo_dict) 373 return; 374 375 const base::ListValue* promo_list = NULL; 376 promo_dict->GetList(PromoTypeToString(promo_type_), &promo_list); 377 if (!promo_list) 378 return; 379 380 const base::DictionaryValue* ntp_promo = NULL; 381 promo_list->GetDictionary(0, &ntp_promo); 382 if (!ntp_promo) 383 return; 384 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()); 389 390 ntp_promo->GetDouble(kPrefPromoStart, &start_); 391 ntp_promo->GetDouble(kPrefPromoEnd, &end_); 392 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_); 398 399 ntp_promo->GetInteger(kPrefPromoMaxViews, &max_views_); 400 ntp_promo->GetInteger(kPrefPromoMaxSeconds, &max_seconds_); 401 ntp_promo->GetDouble(kPrefPromoFirstViewTime, &first_view_time_); 402 403 ntp_promo->GetInteger(kPrefPromoGroup, &group_); 404 ntp_promo->GetInteger(kPrefPromoViews, &views_); 405 ntp_promo->GetBoolean(kPrefPromoClosed, &closed_); 406 } 407 408 bool NotificationPromo::CheckAppLauncher() const { 409 #if !defined(ENABLE_APP_LIST) 410 return true; 411 #else 412 bool is_app_launcher_promo = false; 413 if (!promo_payload_->GetBoolean("is_app_launcher_promo", 414 &is_app_launcher_promo)) 415 return true; 416 return !is_app_launcher_promo || 417 !prefs_->GetBoolean(prefs::kAppLauncherIsEnabled); 418 #endif // !defined(ENABLE_APP_LIST) 419 } 420 421 bool NotificationPromo::CanShow() const { 422 return !closed_ && 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(); 430 } 431 432 // static 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; 439 promo.WritePrefs(); 440 } 441 } 442 443 // static 444 bool NotificationPromo::HandleViewed(PromoType promo_type) { 445 content::RecordAction(UserMetricsAction("NTPPromoShown")); 446 NotificationPromo promo; 447 promo.InitFromPrefs(promo_type); 448 ++promo.views_; 449 if (promo.first_view_time_ == 0) { 450 promo.first_view_time_ = base::Time::Now().ToDoubleT(); 451 } 452 promo.WritePrefs(); 453 return promo.ExceedsMaxViews() || promo.ExceedsMaxSeconds(); 454 } 455 456 bool NotificationPromo::ExceedsMaxGroup() const { 457 return (max_group_ == 0) ? false : group_ >= max_group_; 458 } 459 460 bool NotificationPromo::ExceedsMaxViews() const { 461 return (max_views_ == 0) ? false : views_ >= max_views_; 462 } 463 464 bool NotificationPromo::ExceedsMaxSeconds() const { 465 if (max_seconds_ == 0 || first_view_time_ == 0) 466 return false; 467 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(); 471 } 472 473 // static 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. 482 return url; 483 } 484 485 double NotificationPromo::StartTimeForGroup() const { 486 if (group_ < initial_segment_) 487 return start_; 488 return start_ + 489 std::ceil(static_cast<float>(group_ - initial_segment_ + 1) / increment_) 490 * time_slice_; 491 } 492 493 double NotificationPromo::EndTime() const { 494 return end_; 495 } 496