1 // Copyright (c) 2011 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/promo_resource_service.h" 6 7 #include "base/string_number_conversions.h" 8 #include "base/threading/thread_restrictions.h" 9 #include "base/time.h" 10 #include "base/values.h" 11 #include "chrome/browser/browser_process.h" 12 #include "chrome/browser/extensions/apps_promo.h" 13 #include "chrome/browser/platform_util.h" 14 #include "chrome/browser/prefs/pref_service.h" 15 #include "chrome/browser/profiles/profile.h" 16 #include "chrome/browser/sync/sync_ui_util.h" 17 #include "chrome/common/pref_names.h" 18 #include "content/browser/browser_thread.h" 19 #include "content/common/notification_service.h" 20 #include "content/common/notification_type.h" 21 #include "googleurl/src/gurl.h" 22 23 namespace { 24 25 // Delay on first fetch so we don't interfere with startup. 26 static const int kStartResourceFetchDelay = 5000; 27 28 // Delay between calls to update the cache (48 hours). 29 static const int kCacheUpdateDelay = 48 * 60 * 60 * 1000; 30 31 // Users are randomly assigned to one of kNTPPromoGroupSize buckets, in order 32 // to be able to roll out promos slowly, or display different promos to 33 // different groups. 34 static const int kNTPPromoGroupSize = 16; 35 36 // Maximum number of hours for each time slice (4 weeks). 37 static const int kMaxTimeSliceHours = 24 * 7 * 4; 38 39 // The version of the service (used to expire the cache when upgrading Chrome 40 // to versions with different types of promos). 41 static const int kPromoServiceVersion = 1; 42 43 // Properties used by the server. 44 static const char kAnswerIdProperty[] = "answer_id"; 45 static const char kWebStoreHeaderProperty[] = "question"; 46 static const char kWebStoreButtonProperty[] = "inproduct_target"; 47 static const char kWebStoreLinkProperty[] = "inproduct"; 48 static const char kWebStoreExpireProperty[] = "tooltip"; 49 50 } // namespace 51 52 // Server for dynamically loaded NTP HTML elements. TODO(mirandac): append 53 // locale for future usage, when we're serving localizable strings. 54 const char* PromoResourceService::kDefaultPromoResourceServer = 55 "https://www.google.com/support/chrome/bin/topic/1142433/inproduct?hl="; 56 57 // static 58 void PromoResourceService::RegisterPrefs(PrefService* local_state) { 59 local_state->RegisterIntegerPref(prefs::kNTPPromoVersion, 0); 60 local_state->RegisterStringPref(prefs::kNTPPromoLocale, std::string()); 61 } 62 63 // static 64 void PromoResourceService::RegisterUserPrefs(PrefService* prefs) { 65 prefs->RegisterDoublePref(prefs::kNTPCustomLogoStart, 0); 66 prefs->RegisterDoublePref(prefs::kNTPCustomLogoEnd, 0); 67 prefs->RegisterDoublePref(prefs::kNTPPromoStart, 0); 68 prefs->RegisterDoublePref(prefs::kNTPPromoEnd, 0); 69 prefs->RegisterStringPref(prefs::kNTPPromoLine, std::string()); 70 prefs->RegisterBooleanPref(prefs::kNTPPromoClosed, false); 71 prefs->RegisterIntegerPref(prefs::kNTPPromoGroup, -1); 72 prefs->RegisterIntegerPref(prefs::kNTPPromoBuild, 73 CANARY_BUILD | DEV_BUILD | BETA_BUILD | STABLE_BUILD); 74 prefs->RegisterIntegerPref(prefs::kNTPPromoGroupTimeSlice, 0); 75 } 76 77 // static 78 bool PromoResourceService::IsBuildTargeted(const std::string& channel, 79 int builds_allowed) { 80 if (builds_allowed == NO_BUILD) 81 return false; 82 if (channel == "canary" || channel == "canary-m") { 83 return (CANARY_BUILD & builds_allowed) != 0; 84 } else if (channel == "dev" || channel == "dev-m") { 85 return (DEV_BUILD & builds_allowed) != 0; 86 } else if (channel == "beta" || channel == "beta-m") { 87 return (BETA_BUILD & builds_allowed) != 0; 88 } else if (channel == "" || channel == "m") { 89 return (STABLE_BUILD & builds_allowed) != 0; 90 } else { 91 return false; 92 } 93 } 94 95 PromoResourceService::PromoResourceService(Profile* profile) 96 : WebResourceService(profile, 97 profile->GetPrefs(), 98 PromoResourceService::kDefaultPromoResourceServer, 99 true, // append locale to URL 100 NotificationType::PROMO_RESOURCE_STATE_CHANGED, 101 prefs::kNTPPromoResourceCacheUpdate, 102 kStartResourceFetchDelay, 103 kCacheUpdateDelay), 104 web_resource_cache_(NULL), 105 channel_(NULL) { 106 Init(); 107 } 108 109 PromoResourceService::~PromoResourceService() { } 110 111 void PromoResourceService::Init() { 112 ScheduleNotificationOnInit(); 113 } 114 115 bool PromoResourceService::IsThisBuildTargeted(int builds_targeted) { 116 if (channel_ == NULL) { 117 base::ThreadRestrictions::ScopedAllowIO allow_io; 118 channel_ = platform_util::GetVersionStringModifier().c_str(); 119 } 120 121 return IsBuildTargeted(channel_, builds_targeted); 122 } 123 124 void PromoResourceService::Unpack(const DictionaryValue& parsed_json) { 125 UnpackLogoSignal(parsed_json); 126 UnpackPromoSignal(parsed_json); 127 UnpackWebStoreSignal(parsed_json); 128 } 129 130 void PromoResourceService::ScheduleNotification(double promo_start, 131 double promo_end) { 132 if (promo_start > 0 && promo_end > 0) { 133 int64 ms_until_start = 134 static_cast<int64>((base::Time::FromDoubleT( 135 promo_start) - base::Time::Now()).InMilliseconds()); 136 int64 ms_until_end = 137 static_cast<int64>((base::Time::FromDoubleT( 138 promo_end) - base::Time::Now()).InMilliseconds()); 139 if (ms_until_start > 0) 140 PostNotification(ms_until_start); 141 if (ms_until_end > 0) { 142 PostNotification(ms_until_end); 143 if (ms_until_start <= 0) { 144 // Notify immediately if time is between start and end. 145 PostNotification(0); 146 } 147 } 148 } 149 } 150 151 void PromoResourceService::ScheduleNotificationOnInit() { 152 std::string locale = g_browser_process->GetApplicationLocale(); 153 if ((GetPromoServiceVersion() != kPromoServiceVersion) || 154 (GetPromoLocale() != locale)) { 155 // If the promo service has been upgraded or Chrome switched locales, 156 // refresh the promos. 157 PrefService* local_state = g_browser_process->local_state(); 158 local_state->SetInteger(prefs::kNTPPromoVersion, kPromoServiceVersion); 159 local_state->SetString(prefs::kNTPPromoLocale, locale); 160 prefs_->ClearPref(prefs::kNTPPromoResourceCacheUpdate); 161 AppsPromo::ClearPromo(); 162 PostNotification(0); 163 } else { 164 // If the promo start is in the future, set a notification task to 165 // invalidate the NTP cache at the time of the promo start. 166 double promo_start = prefs_->GetDouble(prefs::kNTPPromoStart); 167 double promo_end = prefs_->GetDouble(prefs::kNTPPromoEnd); 168 ScheduleNotification(promo_start, promo_end); 169 } 170 } 171 172 int PromoResourceService::GetPromoServiceVersion() { 173 PrefService* local_state = g_browser_process->local_state(); 174 return local_state->GetInteger(prefs::kNTPPromoVersion); 175 } 176 177 std::string PromoResourceService::GetPromoLocale() { 178 PrefService* local_state = g_browser_process->local_state(); 179 return local_state->GetString(prefs::kNTPPromoLocale); 180 } 181 182 void PromoResourceService::UnpackPromoSignal( 183 const DictionaryValue& parsed_json) { 184 DictionaryValue* topic_dict; 185 ListValue* answer_list; 186 double old_promo_start = 0; 187 double old_promo_end = 0; 188 double promo_start = 0; 189 double promo_end = 0; 190 191 // Check for preexisting start and end values. 192 if (prefs_->HasPrefPath(prefs::kNTPPromoStart) && 193 prefs_->HasPrefPath(prefs::kNTPPromoEnd)) { 194 old_promo_start = prefs_->GetDouble(prefs::kNTPPromoStart); 195 old_promo_end = prefs_->GetDouble(prefs::kNTPPromoEnd); 196 } 197 198 // Check for newly received start and end values. 199 if (parsed_json.GetDictionary("topic", &topic_dict)) { 200 if (topic_dict->GetList("answers", &answer_list)) { 201 std::string promo_start_string = ""; 202 std::string promo_end_string = ""; 203 std::string promo_string = ""; 204 std::string promo_build = ""; 205 int promo_build_type = 0; 206 int time_slice_hrs = 0; 207 for (ListValue::const_iterator answer_iter = answer_list->begin(); 208 answer_iter != answer_list->end(); ++answer_iter) { 209 if (!(*answer_iter)->IsType(Value::TYPE_DICTIONARY)) 210 continue; 211 DictionaryValue* a_dic = 212 static_cast<DictionaryValue*>(*answer_iter); 213 std::string promo_signal; 214 if (a_dic->GetString("name", &promo_signal)) { 215 if (promo_signal == "promo_start") { 216 a_dic->GetString("question", &promo_build); 217 size_t split = promo_build.find(":"); 218 if (split != std::string::npos && 219 base::StringToInt(promo_build.substr(0, split), 220 &promo_build_type) && 221 base::StringToInt(promo_build.substr(split+1), 222 &time_slice_hrs) && 223 promo_build_type >= 0 && 224 promo_build_type <= (DEV_BUILD | BETA_BUILD | STABLE_BUILD) && 225 time_slice_hrs >= 0 && 226 time_slice_hrs <= kMaxTimeSliceHours) { 227 prefs_->SetInteger(prefs::kNTPPromoBuild, promo_build_type); 228 prefs_->SetInteger(prefs::kNTPPromoGroupTimeSlice, 229 time_slice_hrs); 230 } else { 231 // If no time data or bad time data are set, do not show promo. 232 prefs_->SetInteger(prefs::kNTPPromoBuild, NO_BUILD); 233 prefs_->SetInteger(prefs::kNTPPromoGroupTimeSlice, 0); 234 } 235 a_dic->GetString("inproduct", &promo_start_string); 236 a_dic->GetString("tooltip", &promo_string); 237 prefs_->SetString(prefs::kNTPPromoLine, promo_string); 238 srand(static_cast<uint32>(time(NULL))); 239 prefs_->SetInteger(prefs::kNTPPromoGroup, 240 rand() % kNTPPromoGroupSize); 241 } else if (promo_signal == "promo_end") { 242 a_dic->GetString("inproduct", &promo_end_string); 243 } 244 } 245 } 246 if (!promo_start_string.empty() && 247 promo_start_string.length() > 0 && 248 !promo_end_string.empty() && 249 promo_end_string.length() > 0) { 250 base::Time start_time; 251 base::Time end_time; 252 if (base::Time::FromString( 253 ASCIIToWide(promo_start_string).c_str(), &start_time) && 254 base::Time::FromString( 255 ASCIIToWide(promo_end_string).c_str(), &end_time)) { 256 // Add group time slice, adjusted from hours to seconds. 257 promo_start = start_time.ToDoubleT() + 258 (prefs_->FindPreference(prefs::kNTPPromoGroup) ? 259 prefs_->GetInteger(prefs::kNTPPromoGroup) * 260 time_slice_hrs * 60 * 60 : 0); 261 promo_end = end_time.ToDoubleT(); 262 } 263 } 264 } 265 } 266 267 // If start or end times have changed, trigger a new web resource 268 // notification, so that the logo on the NTP is updated. This check is 269 // outside the reading of the web resource data, because the absence of 270 // dates counts as a triggering change if there were dates before. 271 // Also reset the promo closed preference, to signal a new promo. 272 if (!(old_promo_start == promo_start) || 273 !(old_promo_end == promo_end)) { 274 prefs_->SetDouble(prefs::kNTPPromoStart, promo_start); 275 prefs_->SetDouble(prefs::kNTPPromoEnd, promo_end); 276 prefs_->SetBoolean(prefs::kNTPPromoClosed, false); 277 ScheduleNotification(promo_start, promo_end); 278 } 279 } 280 281 void PromoResourceService::UnpackWebStoreSignal( 282 const DictionaryValue& parsed_json) { 283 DictionaryValue* topic_dict; 284 ListValue* answer_list; 285 286 bool signal_found = false; 287 std::string promo_id = ""; 288 std::string promo_header = ""; 289 std::string promo_button = ""; 290 std::string promo_link = ""; 291 std::string promo_expire = ""; 292 int target_builds = 0; 293 294 if (!parsed_json.GetDictionary("topic", &topic_dict) || 295 !topic_dict->GetList("answers", &answer_list)) 296 return; 297 298 for (ListValue::const_iterator answer_iter = answer_list->begin(); 299 answer_iter != answer_list->end(); ++answer_iter) { 300 if (!(*answer_iter)->IsType(Value::TYPE_DICTIONARY)) 301 continue; 302 DictionaryValue* a_dic = 303 static_cast<DictionaryValue*>(*answer_iter); 304 std::string name; 305 if (!a_dic->GetString("name", &name)) 306 continue; 307 308 size_t split = name.find(":"); 309 if (split == std::string::npos) 310 continue; 311 312 std::string promo_signal = name.substr(0, split); 313 314 if (promo_signal != "webstore_promo" || 315 !base::StringToInt(name.substr(split+1), &target_builds)) 316 continue; 317 318 if (!a_dic->GetString(kAnswerIdProperty, &promo_id) || 319 !a_dic->GetString(kWebStoreHeaderProperty, &promo_header) || 320 !a_dic->GetString(kWebStoreButtonProperty, &promo_button) || 321 !a_dic->GetString(kWebStoreLinkProperty, &promo_link) || 322 !a_dic->GetString(kWebStoreExpireProperty, &promo_expire)) 323 continue; 324 325 if (IsThisBuildTargeted(target_builds)) { 326 // Store the first web store promo that targets the current build. 327 AppsPromo::SetPromo( 328 promo_id, promo_header, promo_button, GURL(promo_link), promo_expire); 329 signal_found = true; 330 break; 331 } 332 } 333 334 if (!signal_found) { 335 // If no web store promos target this build, then clear all the prefs. 336 AppsPromo::ClearPromo(); 337 } 338 339 NotificationService::current()->Notify( 340 NotificationType::WEB_STORE_PROMO_LOADED, 341 Source<PromoResourceService>(this), 342 NotificationService::NoDetails()); 343 344 return; 345 } 346 347 void PromoResourceService::UnpackLogoSignal( 348 const DictionaryValue& parsed_json) { 349 DictionaryValue* topic_dict; 350 ListValue* answer_list; 351 double old_logo_start = 0; 352 double old_logo_end = 0; 353 double logo_start = 0; 354 double logo_end = 0; 355 356 // Check for preexisting start and end values. 357 if (prefs_->HasPrefPath(prefs::kNTPCustomLogoStart) && 358 prefs_->HasPrefPath(prefs::kNTPCustomLogoEnd)) { 359 old_logo_start = prefs_->GetDouble(prefs::kNTPCustomLogoStart); 360 old_logo_end = prefs_->GetDouble(prefs::kNTPCustomLogoEnd); 361 } 362 363 // Check for newly received start and end values. 364 if (parsed_json.GetDictionary("topic", &topic_dict)) { 365 if (topic_dict->GetList("answers", &answer_list)) { 366 std::string logo_start_string = ""; 367 std::string logo_end_string = ""; 368 for (ListValue::const_iterator answer_iter = answer_list->begin(); 369 answer_iter != answer_list->end(); ++answer_iter) { 370 if (!(*answer_iter)->IsType(Value::TYPE_DICTIONARY)) 371 continue; 372 DictionaryValue* a_dic = 373 static_cast<DictionaryValue*>(*answer_iter); 374 std::string logo_signal; 375 if (a_dic->GetString("name", &logo_signal)) { 376 if (logo_signal == "custom_logo_start") { 377 a_dic->GetString("inproduct", &logo_start_string); 378 } else if (logo_signal == "custom_logo_end") { 379 a_dic->GetString("inproduct", &logo_end_string); 380 } 381 } 382 } 383 if (!logo_start_string.empty() && 384 logo_start_string.length() > 0 && 385 !logo_end_string.empty() && 386 logo_end_string.length() > 0) { 387 base::Time start_time; 388 base::Time end_time; 389 if (base::Time::FromString( 390 ASCIIToWide(logo_start_string).c_str(), &start_time) && 391 base::Time::FromString( 392 ASCIIToWide(logo_end_string).c_str(), &end_time)) { 393 logo_start = start_time.ToDoubleT(); 394 logo_end = end_time.ToDoubleT(); 395 } 396 } 397 } 398 } 399 400 // If logo start or end times have changed, trigger a new web resource 401 // notification, so that the logo on the NTP is updated. This check is 402 // outside the reading of the web resource data, because the absence of 403 // dates counts as a triggering change if there were dates before. 404 if (!(old_logo_start == logo_start) || 405 !(old_logo_end == logo_end)) { 406 prefs_->SetDouble(prefs::kNTPCustomLogoStart, logo_start); 407 prefs_->SetDouble(prefs::kNTPCustomLogoEnd, logo_end); 408 NotificationService* service = NotificationService::current(); 409 service->Notify(NotificationType::PROMO_RESOURCE_STATE_CHANGED, 410 Source<WebResourceService>(this), 411 NotificationService::NoDetails()); 412 } 413 } 414 415 namespace PromoResourceServiceUtil { 416 417 bool CanShowPromo(Profile* profile) { 418 bool promo_closed = false; 419 PrefService* prefs = profile->GetPrefs(); 420 if (prefs->HasPrefPath(prefs::kNTPPromoClosed)) 421 promo_closed = prefs->GetBoolean(prefs::kNTPPromoClosed); 422 423 // Only show if not synced. 424 bool is_synced = 425 (profile->HasProfileSyncService() && 426 sync_ui_util::GetStatus( 427 profile->GetProfileSyncService()) == sync_ui_util::SYNCED); 428 429 bool is_promo_build = false; 430 if (prefs->HasPrefPath(prefs::kNTPPromoBuild)) { 431 // GetVersionStringModifier hits the registry. See http://crbug.com/70898. 432 base::ThreadRestrictions::ScopedAllowIO allow_io; 433 const std::string channel = platform_util::GetVersionStringModifier(); 434 is_promo_build = PromoResourceService::IsBuildTargeted( 435 channel, prefs->GetInteger(prefs::kNTPPromoBuild)); 436 } 437 438 return !promo_closed && !is_synced && is_promo_build; 439 } 440 441 } // namespace PromoResourceServiceUtil 442