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