notification_promo.cc revision 5821806d5e7f356e8fa4b058a389a808ea183019
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/rand_util.h" 12#include "base/string_number_conversions.h" 13#include "base/string_util.h" 14#include "base/sys_info.h" 15#include "base/time.h" 16#include "base/values.h" 17#include "chrome/browser/prefs/pref_service.h" 18#include "chrome/browser/profiles/profile_impl.h" 19#include "chrome/browser/web_resource/promo_resource_service.h" 20#include "chrome/common/chrome_version_info.h" 21#include "chrome/common/net/url_util.h" 22#include "chrome/common/pref_names.h" 23#include "content/public/browser/user_metrics.h" 24#include "googleurl/src/gurl.h" 25 26#if defined(OS_ANDROID) 27#include "base/command_line.h" 28#include "chrome/common/chrome_switches.h" 29#endif // defined(OS_ANDROID) 30 31using content::UserMetricsAction; 32 33namespace { 34 35const int kDefaultGroupSize = 100; 36 37const char promo_server_url[] = "https://clients3.google.com/crsignal/client"; 38 39// The name of the preference that stores the promotion object. 40const char kPrefPromoObject[] = "promo"; 41 42// Keys in the kPrefPromoObject dictionary; used only here. 43const char kPrefPromoText[] = "text"; 44const char kPrefPromoPayload[] = "payload"; 45const char kPrefPromoStart[] = "start"; 46const char kPrefPromoEnd[] = "end"; 47const char kPrefPromoNumGroups[] = "num_groups"; 48const char kPrefPromoSegment[] = "segment"; 49const char kPrefPromoIncrement[] = "increment"; 50const char kPrefPromoIncrementFrequency[] = "increment_frequency"; 51const char kPrefPromoIncrementMax[] = "increment_max"; 52const char kPrefPromoMaxViews[] = "max_views"; 53const char kPrefPromoGroup[] = "group"; 54const char kPrefPromoViews[] = "views"; 55const char kPrefPromoClosed[] = "closed"; 56const char kPrefPromoGPlusRequired[] = "gplus_required"; 57 58// Returns a string suitable for the Promo Server URL 'osname' value. 59std::string PlatformString() { 60#if defined(OS_WIN) 61 return "win"; 62#elif defined(OS_IOS) 63 // TODO(noyau): add iOS-specific implementation 64 const bool isTablet = false; 65 return std::string("ios-") + (isTablet ? "tablet" : "phone"); 66#elif defined(OS_MACOSX) 67 return "mac"; 68#elif defined(OS_CHROMEOS) 69 return "chromeos"; 70#elif defined(OS_LINUX) 71 return "linux"; 72#elif defined(OS_ANDROID) 73 const bool isTablet = 74 CommandLine::ForCurrentProcess()->HasSwitch(switches::kTabletUI); 75 return std::string("android-") + (isTablet ? "tablet" : "phone"); 76#else 77 return "none"; 78#endif 79} 80 81// Returns a string suitable for the Promo Server URL 'dist' value. 82const 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 104struct PromoMapEntry { 105 NotificationPromo::PromoType promo_type; 106 const char* promo_type_str; 107}; 108 109const 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}; 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(); ++it) { 145 base::Value* child_copy = DeepCopyAndResolveStrings(*it, strings); 146 copy->Append(child_copy); 147 } 148 return copy; 149 } 150 151 case Value::TYPE_DICTIONARY: { 152 const base::DictionaryValue* dict = 153 static_cast<const base::DictionaryValue*>(node); 154 base::DictionaryValue* copy = new base::DictionaryValue; 155 for (base::DictionaryValue::key_iterator it = dict->begin_keys(); 156 it != dict->end_keys(); ++it) { 157 const base::Value* child = NULL; 158 bool rv = dict->GetWithoutPathExpansion(*it, &child); 159 DCHECK(rv); 160 base::Value* child_copy = DeepCopyAndResolveStrings(child, strings); 161 copy->SetWithoutPathExpansion(*it, child_copy); 162 } 163 return copy; 164 } 165 166 case Value::TYPE_STRING: { 167 const base::StringValue* str = 168 static_cast<const base::StringValue*>(node); 169 std::string value; 170 bool rv = str->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 184void AppendQueryParameter(GURL* url, 185 const std::string& param, 186 const std::string& value) { 187 *url = chrome_common_net::AppendQueryParameter(*url, param, value); 188} 189 190} // namespace 191 192NotificationPromo::NotificationPromo(Profile* profile) 193 : profile_(profile), 194 prefs_(profile_->GetPrefs()), 195 promo_type_(NO_PROMO), 196 promo_payload_(new base::DictionaryValue()), 197 start_(0.0), 198 end_(0.0), 199 num_groups_(kDefaultGroupSize), 200 initial_segment_(0), 201 increment_(1), 202 time_slice_(0), 203 max_group_(0), 204 max_views_(0), 205 group_(0), 206 views_(0), 207 closed_(false), 208 gplus_required_(false), 209 new_notification_(false) { 210 DCHECK(profile); 211 DCHECK(prefs_); 212} 213 214NotificationPromo::~NotificationPromo() {} 215 216void NotificationPromo::InitFromJson(const DictionaryValue& json, 217 PromoType promo_type) { 218 promo_type_ = promo_type; 219 const ListValue* promo_list = NULL; 220 DVLOG(1) << "InitFromJson " << PromoTypeToString(promo_type_); 221 if (!json.GetList(PromoTypeToString(promo_type_), &promo_list)) 222 return; 223 224 // No support for multiple promos yet. Only consider the first one. 225 const DictionaryValue* promo = NULL; 226 if (!promo_list->GetDictionary(0, &promo)) 227 return; 228 229 // Date. 230 const ListValue* date_list = NULL; 231 if (promo->GetList("date", &date_list)) { 232 const DictionaryValue* date; 233 if (date_list->GetDictionary(0, &date)) { 234 std::string time_str; 235 base::Time time; 236 if (date->GetString("start", &time_str) && 237 base::Time::FromString(time_str.c_str(), &time)) { 238 start_ = time.ToDoubleT(); 239 DVLOG(1) << "start str=" << time_str 240 << ", start_="<< base::DoubleToString(start_); 241 } 242 if (date->GetString("end", &time_str) && 243 base::Time::FromString(time_str.c_str(), &time)) { 244 end_ = time.ToDoubleT(); 245 DVLOG(1) << "end str =" << time_str 246 << ", end_=" << base::DoubleToString(end_); 247 } 248 } 249 } 250 251 // Grouping. 252 const DictionaryValue* grouping = NULL; 253 if (promo->GetDictionary("grouping", &grouping)) { 254 grouping->GetInteger("buckets", &num_groups_); 255 grouping->GetInteger("segment", &initial_segment_); 256 grouping->GetInteger("increment", &increment_); 257 grouping->GetInteger("increment_frequency", &time_slice_); 258 grouping->GetInteger("increment_max", &max_group_); 259 260 DVLOG(1) << "num_groups_ = " << num_groups_ 261 << ", initial_segment_ = " << initial_segment_ 262 << ", increment_ = " << increment_ 263 << ", time_slice_ = " << time_slice_ 264 << ", max_group_ = " << max_group_; 265 } 266 267 // Strings. 268 const DictionaryValue* strings = NULL; 269 promo->GetDictionary("strings", &strings); 270 271 // Payload. 272 const DictionaryValue* payload = NULL; 273 if (promo->GetDictionary("payload", &payload)) { 274 payload->GetBoolean("gplus_required", &gplus_required_); 275 DVLOG(1) << "gplus_required_ = " << gplus_required_; 276 277 base::Value* ppcopy = DeepCopyAndResolveStrings(payload, strings); 278 DCHECK(ppcopy && ppcopy->IsType(base::Value::TYPE_DICTIONARY)); 279 promo_payload_.reset(static_cast<base::DictionaryValue*>(ppcopy)); 280 } 281 282 if (!promo_payload_->GetString("promo_message_short", &promo_text_) && 283 strings) { 284 // For compatibility with the legacy desktop version, 285 // if no |payload.promo_message_short| is specified, 286 // the first string in |strings| is used. 287 DictionaryValue::Iterator iter(*strings); 288 iter.value().GetAsString(&promo_text_); 289 } 290 DVLOG(1) << "promo_text_=" << promo_text_; 291 292 promo->GetInteger("max_views", &max_views_); 293 DVLOG(1) << "max_views_ " << max_views_; 294 295 CheckForNewNotification(); 296} 297 298void NotificationPromo::CheckForNewNotification() { 299 NotificationPromo old_promo(profile_); 300 old_promo.InitFromPrefs(promo_type_); 301 const double old_start = old_promo.start_; 302 const double old_end = old_promo.end_; 303 const std::string old_promo_text = old_promo.promo_text_; 304 305 new_notification_ = 306 old_start != start_ || old_end != end_ || old_promo_text != promo_text_; 307 if (new_notification_) 308 OnNewNotification(); 309} 310 311void NotificationPromo::OnNewNotification() { 312 DVLOG(1) << "OnNewNotification"; 313 // Create a new promo group. 314 group_ = base::RandInt(0, num_groups_ - 1); 315 WritePrefs(); 316} 317 318// static 319void NotificationPromo::RegisterUserPrefs(PrefService* prefs) { 320 prefs->RegisterDictionaryPref(kPrefPromoObject, 321 new base::DictionaryValue, 322 PrefService::UNSYNCABLE_PREF); 323} 324 325void NotificationPromo::WritePrefs() { 326 base::DictionaryValue* ntp_promo = new base::DictionaryValue; 327 ntp_promo->SetString(kPrefPromoText, promo_text_); 328 ntp_promo->Set(kPrefPromoPayload, promo_payload_->DeepCopy()); 329 ntp_promo->SetDouble(kPrefPromoStart, start_); 330 ntp_promo->SetDouble(kPrefPromoEnd, end_); 331 332 ntp_promo->SetInteger(kPrefPromoNumGroups, num_groups_); 333 ntp_promo->SetInteger(kPrefPromoSegment, initial_segment_); 334 ntp_promo->SetInteger(kPrefPromoIncrement, increment_); 335 ntp_promo->SetInteger(kPrefPromoIncrementFrequency, time_slice_); 336 ntp_promo->SetInteger(kPrefPromoIncrementMax, max_group_); 337 338 ntp_promo->SetInteger(kPrefPromoMaxViews, max_views_); 339 340 ntp_promo->SetInteger(kPrefPromoGroup, group_); 341 ntp_promo->SetInteger(kPrefPromoViews, views_); 342 ntp_promo->SetBoolean(kPrefPromoClosed, closed_); 343 344 ntp_promo->SetBoolean(kPrefPromoGPlusRequired, gplus_required_); 345 346 base::ListValue* promo_list = new base::ListValue; 347 promo_list->Set(0, ntp_promo); // Only support 1 promo for now. 348 349 base::DictionaryValue promo_dict; 350 promo_dict.MergeDictionary(prefs_->GetDictionary(kPrefPromoObject)); 351 promo_dict.Set(PromoTypeToString(promo_type_), promo_list); 352 prefs_->Set(kPrefPromoObject, promo_dict); 353 DVLOG(1) << "WritePrefs " << promo_dict; 354} 355 356void NotificationPromo::InitFromPrefs(PromoType promo_type) { 357 promo_type_ = promo_type; 358 const base::DictionaryValue* promo_dict = 359 prefs_->GetDictionary(kPrefPromoObject); 360 if (!promo_dict) 361 return; 362 363 const base::ListValue* promo_list = NULL; 364 promo_dict->GetList(PromoTypeToString(promo_type_), &promo_list); 365 if (!promo_list) 366 return; 367 368 const base::DictionaryValue* ntp_promo = NULL; 369 promo_list->GetDictionary(0, &ntp_promo); 370 if (!ntp_promo) 371 return; 372 373 ntp_promo->GetString(kPrefPromoText, &promo_text_); 374 const base::DictionaryValue* promo_payload = NULL; 375 if (ntp_promo->GetDictionary(kPrefPromoPayload, &promo_payload)) 376 promo_payload_.reset(promo_payload->DeepCopy()); 377 378 ntp_promo->GetDouble(kPrefPromoStart, &start_); 379 ntp_promo->GetDouble(kPrefPromoEnd, &end_); 380 381 ntp_promo->GetInteger(kPrefPromoNumGroups, &num_groups_); 382 ntp_promo->GetInteger(kPrefPromoSegment, &initial_segment_); 383 ntp_promo->GetInteger(kPrefPromoIncrement, &increment_); 384 ntp_promo->GetInteger(kPrefPromoIncrementFrequency, &time_slice_); 385 ntp_promo->GetInteger(kPrefPromoIncrementMax, &max_group_); 386 387 ntp_promo->GetInteger(kPrefPromoMaxViews, &max_views_); 388 389 ntp_promo->GetInteger(kPrefPromoGroup, &group_); 390 ntp_promo->GetInteger(kPrefPromoViews, &views_); 391 ntp_promo->GetBoolean(kPrefPromoClosed, &closed_); 392 393 ntp_promo->GetBoolean(kPrefPromoGPlusRequired, &gplus_required_); 394} 395 396bool NotificationPromo::CanShow() const { 397 return !closed_ && 398 !promo_text_.empty() && 399 !ExceedsMaxGroup() && 400 !ExceedsMaxViews() && 401 base::Time::FromDoubleT(StartTimeForGroup()) < base::Time::Now() && 402 base::Time::FromDoubleT(EndTime()) > base::Time::Now() && 403 IsGPlusRequired(); 404} 405 406// static 407void NotificationPromo::HandleClosed(Profile* profile, PromoType promo_type) { 408 content::RecordAction(UserMetricsAction("NTPPromoClosed")); 409 NotificationPromo promo(profile); 410 promo.InitFromPrefs(promo_type); 411 if (!promo.closed_) { 412 promo.closed_ = true; 413 promo.WritePrefs(); 414 } 415} 416 417// static 418bool NotificationPromo::HandleViewed(Profile* profile, PromoType promo_type) { 419 content::RecordAction(UserMetricsAction("NTPPromoShown")); 420 NotificationPromo promo(profile); 421 promo.InitFromPrefs(promo_type); 422 ++promo.views_; 423 promo.WritePrefs(); 424 return promo.ExceedsMaxViews(); 425} 426 427bool NotificationPromo::ExceedsMaxGroup() const { 428 return (max_group_ == 0) ? false : group_ >= max_group_; 429} 430 431bool NotificationPromo::ExceedsMaxViews() const { 432 return (max_views_ == 0) ? false : views_ >= max_views_; 433} 434 435bool NotificationPromo::IsGPlusRequired() const { 436 return !gplus_required_ || prefs_->GetBoolean(prefs::kIsGooglePlusUser); 437} 438 439// static 440GURL NotificationPromo::PromoServerURL() { 441 GURL url(promo_server_url); 442 AppendQueryParameter(&url, "dist", ChannelString()); 443 AppendQueryParameter(&url, "osname", PlatformString()); 444 AppendQueryParameter(&url, "branding", chrome::VersionInfo().Version()); 445 AppendQueryParameter(&url, "osver", base::SysInfo::OperatingSystemVersion()); 446 DVLOG(1) << "PromoServerURL=" << url.spec(); 447 // Note that locale param is added by WebResourceService. 448 return url; 449} 450 451double NotificationPromo::StartTimeForGroup() const { 452 if (group_ < initial_segment_) 453 return start_; 454 return start_ + 455 std::ceil(static_cast<float>(group_ - initial_segment_ + 1) / increment_) 456 * time_slice_; 457} 458 459double NotificationPromo::EndTime() const { 460 return end_; 461} 462