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