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
RegisterPrefs(PrefService * local_state)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
RegisterUserPrefs(PrefService * prefs)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
IsBuildTargeted(const std::string & channel,int builds_allowed)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
PromoResourceService(Profile * profile)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
~PromoResourceService()109 PromoResourceService::~PromoResourceService() { }
110
Init()111 void PromoResourceService::Init() {
112 ScheduleNotificationOnInit();
113 }
114
IsThisBuildTargeted(int builds_targeted)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
Unpack(const DictionaryValue & parsed_json)124 void PromoResourceService::Unpack(const DictionaryValue& parsed_json) {
125 UnpackLogoSignal(parsed_json);
126 UnpackPromoSignal(parsed_json);
127 UnpackWebStoreSignal(parsed_json);
128 }
129
ScheduleNotification(double promo_start,double promo_end)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
ScheduleNotificationOnInit()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
GetPromoServiceVersion()172 int PromoResourceService::GetPromoServiceVersion() {
173 PrefService* local_state = g_browser_process->local_state();
174 return local_state->GetInteger(prefs::kNTPPromoVersion);
175 }
176
GetPromoLocale()177 std::string PromoResourceService::GetPromoLocale() {
178 PrefService* local_state = g_browser_process->local_state();
179 return local_state->GetString(prefs::kNTPPromoLocale);
180 }
181
UnpackPromoSignal(const DictionaryValue & parsed_json)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
UnpackWebStoreSignal(const DictionaryValue & parsed_json)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
UnpackLogoSignal(const DictionaryValue & parsed_json)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
CanShowPromo(Profile * profile)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