// 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