• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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