// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/web_resource/promo_resource_service.h" #include "base/string_number_conversions.h" #include "base/threading/thread_restrictions.h" #include "base/time.h" #include "base/values.h" #include "chrome/browser/browser_process.h" #include "chrome/browser/extensions/apps_promo.h" #include "chrome/browser/platform_util.h" #include "chrome/browser/prefs/pref_service.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/sync/sync_ui_util.h" #include "chrome/common/pref_names.h" #include "content/browser/browser_thread.h" #include "content/common/notification_service.h" #include "content/common/notification_type.h" #include "googleurl/src/gurl.h" namespace { // Delay on first fetch so we don't interfere with startup. static const int kStartResourceFetchDelay = 5000; // Delay between calls to update the cache (48 hours). static const int kCacheUpdateDelay = 48 * 60 * 60 * 1000; // Users are randomly assigned to one of kNTPPromoGroupSize buckets, in order // to be able to roll out promos slowly, or display different promos to // different groups. static const int kNTPPromoGroupSize = 16; // Maximum number of hours for each time slice (4 weeks). static const int kMaxTimeSliceHours = 24 * 7 * 4; // The version of the service (used to expire the cache when upgrading Chrome // to versions with different types of promos). static const int kPromoServiceVersion = 1; // Properties used by the server. static const char kAnswerIdProperty[] = "answer_id"; static const char kWebStoreHeaderProperty[] = "question"; static const char kWebStoreButtonProperty[] = "inproduct_target"; static const char kWebStoreLinkProperty[] = "inproduct"; static const char kWebStoreExpireProperty[] = "tooltip"; } // namespace // Server for dynamically loaded NTP HTML elements. TODO(mirandac): append // locale for future usage, when we're serving localizable strings. const char* PromoResourceService::kDefaultPromoResourceServer = "https://www.google.com/support/chrome/bin/topic/1142433/inproduct?hl="; // static void PromoResourceService::RegisterPrefs(PrefService* local_state) { local_state->RegisterIntegerPref(prefs::kNTPPromoVersion, 0); local_state->RegisterStringPref(prefs::kNTPPromoLocale, std::string()); } // static void PromoResourceService::RegisterUserPrefs(PrefService* prefs) { prefs->RegisterDoublePref(prefs::kNTPCustomLogoStart, 0); prefs->RegisterDoublePref(prefs::kNTPCustomLogoEnd, 0); prefs->RegisterDoublePref(prefs::kNTPPromoStart, 0); prefs->RegisterDoublePref(prefs::kNTPPromoEnd, 0); prefs->RegisterStringPref(prefs::kNTPPromoLine, std::string()); prefs->RegisterBooleanPref(prefs::kNTPPromoClosed, false); prefs->RegisterIntegerPref(prefs::kNTPPromoGroup, -1); prefs->RegisterIntegerPref(prefs::kNTPPromoBuild, CANARY_BUILD | DEV_BUILD | BETA_BUILD | STABLE_BUILD); prefs->RegisterIntegerPref(prefs::kNTPPromoGroupTimeSlice, 0); } // static bool PromoResourceService::IsBuildTargeted(const std::string& channel, int builds_allowed) { if (builds_allowed == NO_BUILD) return false; if (channel == "canary" || channel == "canary-m") { return (CANARY_BUILD & builds_allowed) != 0; } else if (channel == "dev" || channel == "dev-m") { return (DEV_BUILD & builds_allowed) != 0; } else if (channel == "beta" || channel == "beta-m") { return (BETA_BUILD & builds_allowed) != 0; } else if (channel == "" || channel == "m") { return (STABLE_BUILD & builds_allowed) != 0; } else { return false; } } PromoResourceService::PromoResourceService(Profile* profile) : WebResourceService(profile, profile->GetPrefs(), PromoResourceService::kDefaultPromoResourceServer, true, // append locale to URL NotificationType::PROMO_RESOURCE_STATE_CHANGED, prefs::kNTPPromoResourceCacheUpdate, kStartResourceFetchDelay, kCacheUpdateDelay), web_resource_cache_(NULL), channel_(NULL) { Init(); } PromoResourceService::~PromoResourceService() { } void PromoResourceService::Init() { ScheduleNotificationOnInit(); } bool PromoResourceService::IsThisBuildTargeted(int builds_targeted) { if (channel_ == NULL) { base::ThreadRestrictions::ScopedAllowIO allow_io; channel_ = platform_util::GetVersionStringModifier().c_str(); } return IsBuildTargeted(channel_, builds_targeted); } void PromoResourceService::Unpack(const DictionaryValue& parsed_json) { UnpackLogoSignal(parsed_json); UnpackPromoSignal(parsed_json); UnpackWebStoreSignal(parsed_json); } void PromoResourceService::ScheduleNotification(double promo_start, double promo_end) { if (promo_start > 0 && promo_end > 0) { int64 ms_until_start = static_cast<int64>((base::Time::FromDoubleT( promo_start) - base::Time::Now()).InMilliseconds()); int64 ms_until_end = static_cast<int64>((base::Time::FromDoubleT( promo_end) - base::Time::Now()).InMilliseconds()); if (ms_until_start > 0) PostNotification(ms_until_start); if (ms_until_end > 0) { PostNotification(ms_until_end); if (ms_until_start <= 0) { // Notify immediately if time is between start and end. PostNotification(0); } } } } void PromoResourceService::ScheduleNotificationOnInit() { std::string locale = g_browser_process->GetApplicationLocale(); if ((GetPromoServiceVersion() != kPromoServiceVersion) || (GetPromoLocale() != locale)) { // If the promo service has been upgraded or Chrome switched locales, // refresh the promos. PrefService* local_state = g_browser_process->local_state(); local_state->SetInteger(prefs::kNTPPromoVersion, kPromoServiceVersion); local_state->SetString(prefs::kNTPPromoLocale, locale); prefs_->ClearPref(prefs::kNTPPromoResourceCacheUpdate); AppsPromo::ClearPromo(); PostNotification(0); } else { // If the promo start is in the future, set a notification task to // invalidate the NTP cache at the time of the promo start. double promo_start = prefs_->GetDouble(prefs::kNTPPromoStart); double promo_end = prefs_->GetDouble(prefs::kNTPPromoEnd); ScheduleNotification(promo_start, promo_end); } } int PromoResourceService::GetPromoServiceVersion() { PrefService* local_state = g_browser_process->local_state(); return local_state->GetInteger(prefs::kNTPPromoVersion); } std::string PromoResourceService::GetPromoLocale() { PrefService* local_state = g_browser_process->local_state(); return local_state->GetString(prefs::kNTPPromoLocale); } void PromoResourceService::UnpackPromoSignal( const DictionaryValue& parsed_json) { DictionaryValue* topic_dict; ListValue* answer_list; double old_promo_start = 0; double old_promo_end = 0; double promo_start = 0; double promo_end = 0; // Check for preexisting start and end values. if (prefs_->HasPrefPath(prefs::kNTPPromoStart) && prefs_->HasPrefPath(prefs::kNTPPromoEnd)) { old_promo_start = prefs_->GetDouble(prefs::kNTPPromoStart); old_promo_end = prefs_->GetDouble(prefs::kNTPPromoEnd); } // Check for newly received start and end values. if (parsed_json.GetDictionary("topic", &topic_dict)) { if (topic_dict->GetList("answers", &answer_list)) { std::string promo_start_string = ""; std::string promo_end_string = ""; std::string promo_string = ""; std::string promo_build = ""; int promo_build_type = 0; int time_slice_hrs = 0; for (ListValue::const_iterator answer_iter = answer_list->begin(); answer_iter != answer_list->end(); ++answer_iter) { if (!(*answer_iter)->IsType(Value::TYPE_DICTIONARY)) continue; DictionaryValue* a_dic = static_cast<DictionaryValue*>(*answer_iter); std::string promo_signal; if (a_dic->GetString("name", &promo_signal)) { if (promo_signal == "promo_start") { a_dic->GetString("question", &promo_build); size_t split = promo_build.find(":"); if (split != std::string::npos && base::StringToInt(promo_build.substr(0, split), &promo_build_type) && base::StringToInt(promo_build.substr(split+1), &time_slice_hrs) && promo_build_type >= 0 && promo_build_type <= (DEV_BUILD | BETA_BUILD | STABLE_BUILD) && time_slice_hrs >= 0 && time_slice_hrs <= kMaxTimeSliceHours) { prefs_->SetInteger(prefs::kNTPPromoBuild, promo_build_type); prefs_->SetInteger(prefs::kNTPPromoGroupTimeSlice, time_slice_hrs); } else { // If no time data or bad time data are set, do not show promo. prefs_->SetInteger(prefs::kNTPPromoBuild, NO_BUILD); prefs_->SetInteger(prefs::kNTPPromoGroupTimeSlice, 0); } a_dic->GetString("inproduct", &promo_start_string); a_dic->GetString("tooltip", &promo_string); prefs_->SetString(prefs::kNTPPromoLine, promo_string); srand(static_cast<uint32>(time(NULL))); prefs_->SetInteger(prefs::kNTPPromoGroup, rand() % kNTPPromoGroupSize); } else if (promo_signal == "promo_end") { a_dic->GetString("inproduct", &promo_end_string); } } } if (!promo_start_string.empty() && promo_start_string.length() > 0 && !promo_end_string.empty() && promo_end_string.length() > 0) { base::Time start_time; base::Time end_time; if (base::Time::FromString( ASCIIToWide(promo_start_string).c_str(), &start_time) && base::Time::FromString( ASCIIToWide(promo_end_string).c_str(), &end_time)) { // Add group time slice, adjusted from hours to seconds. promo_start = start_time.ToDoubleT() + (prefs_->FindPreference(prefs::kNTPPromoGroup) ? prefs_->GetInteger(prefs::kNTPPromoGroup) * time_slice_hrs * 60 * 60 : 0); promo_end = end_time.ToDoubleT(); } } } } // If start or end times have changed, trigger a new web resource // notification, so that the logo on the NTP is updated. This check is // outside the reading of the web resource data, because the absence of // dates counts as a triggering change if there were dates before. // Also reset the promo closed preference, to signal a new promo. if (!(old_promo_start == promo_start) || !(old_promo_end == promo_end)) { prefs_->SetDouble(prefs::kNTPPromoStart, promo_start); prefs_->SetDouble(prefs::kNTPPromoEnd, promo_end); prefs_->SetBoolean(prefs::kNTPPromoClosed, false); ScheduleNotification(promo_start, promo_end); } } void PromoResourceService::UnpackWebStoreSignal( const DictionaryValue& parsed_json) { DictionaryValue* topic_dict; ListValue* answer_list; bool signal_found = false; std::string promo_id = ""; std::string promo_header = ""; std::string promo_button = ""; std::string promo_link = ""; std::string promo_expire = ""; int target_builds = 0; if (!parsed_json.GetDictionary("topic", &topic_dict) || !topic_dict->GetList("answers", &answer_list)) return; for (ListValue::const_iterator answer_iter = answer_list->begin(); answer_iter != answer_list->end(); ++answer_iter) { if (!(*answer_iter)->IsType(Value::TYPE_DICTIONARY)) continue; DictionaryValue* a_dic = static_cast<DictionaryValue*>(*answer_iter); std::string name; if (!a_dic->GetString("name", &name)) continue; size_t split = name.find(":"); if (split == std::string::npos) continue; std::string promo_signal = name.substr(0, split); if (promo_signal != "webstore_promo" || !base::StringToInt(name.substr(split+1), &target_builds)) continue; if (!a_dic->GetString(kAnswerIdProperty, &promo_id) || !a_dic->GetString(kWebStoreHeaderProperty, &promo_header) || !a_dic->GetString(kWebStoreButtonProperty, &promo_button) || !a_dic->GetString(kWebStoreLinkProperty, &promo_link) || !a_dic->GetString(kWebStoreExpireProperty, &promo_expire)) continue; if (IsThisBuildTargeted(target_builds)) { // Store the first web store promo that targets the current build. AppsPromo::SetPromo( promo_id, promo_header, promo_button, GURL(promo_link), promo_expire); signal_found = true; break; } } if (!signal_found) { // If no web store promos target this build, then clear all the prefs. AppsPromo::ClearPromo(); } NotificationService::current()->Notify( NotificationType::WEB_STORE_PROMO_LOADED, Source<PromoResourceService>(this), NotificationService::NoDetails()); return; } void PromoResourceService::UnpackLogoSignal( const DictionaryValue& parsed_json) { DictionaryValue* topic_dict; ListValue* answer_list; double old_logo_start = 0; double old_logo_end = 0; double logo_start = 0; double logo_end = 0; // Check for preexisting start and end values. if (prefs_->HasPrefPath(prefs::kNTPCustomLogoStart) && prefs_->HasPrefPath(prefs::kNTPCustomLogoEnd)) { old_logo_start = prefs_->GetDouble(prefs::kNTPCustomLogoStart); old_logo_end = prefs_->GetDouble(prefs::kNTPCustomLogoEnd); } // Check for newly received start and end values. if (parsed_json.GetDictionary("topic", &topic_dict)) { if (topic_dict->GetList("answers", &answer_list)) { std::string logo_start_string = ""; std::string logo_end_string = ""; for (ListValue::const_iterator answer_iter = answer_list->begin(); answer_iter != answer_list->end(); ++answer_iter) { if (!(*answer_iter)->IsType(Value::TYPE_DICTIONARY)) continue; DictionaryValue* a_dic = static_cast<DictionaryValue*>(*answer_iter); std::string logo_signal; if (a_dic->GetString("name", &logo_signal)) { if (logo_signal == "custom_logo_start") { a_dic->GetString("inproduct", &logo_start_string); } else if (logo_signal == "custom_logo_end") { a_dic->GetString("inproduct", &logo_end_string); } } } if (!logo_start_string.empty() && logo_start_string.length() > 0 && !logo_end_string.empty() && logo_end_string.length() > 0) { base::Time start_time; base::Time end_time; if (base::Time::FromString( ASCIIToWide(logo_start_string).c_str(), &start_time) && base::Time::FromString( ASCIIToWide(logo_end_string).c_str(), &end_time)) { logo_start = start_time.ToDoubleT(); logo_end = end_time.ToDoubleT(); } } } } // If logo start or end times have changed, trigger a new web resource // notification, so that the logo on the NTP is updated. This check is // outside the reading of the web resource data, because the absence of // dates counts as a triggering change if there were dates before. if (!(old_logo_start == logo_start) || !(old_logo_end == logo_end)) { prefs_->SetDouble(prefs::kNTPCustomLogoStart, logo_start); prefs_->SetDouble(prefs::kNTPCustomLogoEnd, logo_end); NotificationService* service = NotificationService::current(); service->Notify(NotificationType::PROMO_RESOURCE_STATE_CHANGED, Source<WebResourceService>(this), NotificationService::NoDetails()); } } namespace PromoResourceServiceUtil { bool CanShowPromo(Profile* profile) { bool promo_closed = false; PrefService* prefs = profile->GetPrefs(); if (prefs->HasPrefPath(prefs::kNTPPromoClosed)) promo_closed = prefs->GetBoolean(prefs::kNTPPromoClosed); // Only show if not synced. bool is_synced = (profile->HasProfileSyncService() && sync_ui_util::GetStatus( profile->GetProfileSyncService()) == sync_ui_util::SYNCED); bool is_promo_build = false; if (prefs->HasPrefPath(prefs::kNTPPromoBuild)) { // GetVersionStringModifier hits the registry. See http://crbug.com/70898. base::ThreadRestrictions::ScopedAllowIO allow_io; const std::string channel = platform_util::GetVersionStringModifier(); is_promo_build = PromoResourceService::IsBuildTargeted( channel, prefs->GetInteger(prefs::kNTPPromoBuild)); } return !promo_closed && !is_synced && is_promo_build; } } // namespace PromoResourceServiceUtil