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
23namespace {
24
25// Delay on first fetch so we don't interfere with startup.
26static const int kStartResourceFetchDelay = 5000;
27
28// Delay between calls to update the cache (48 hours).
29static 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.
34static const int kNTPPromoGroupSize = 16;
35
36// Maximum number of hours for each time slice (4 weeks).
37static 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).
41static const int kPromoServiceVersion = 1;
42
43// Properties used by the server.
44static const char kAnswerIdProperty[] = "answer_id";
45static const char kWebStoreHeaderProperty[] = "question";
46static const char kWebStoreButtonProperty[] = "inproduct_target";
47static const char kWebStoreLinkProperty[] = "inproduct";
48static 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.
54const char* PromoResourceService::kDefaultPromoResourceServer =
55    "https://www.google.com/support/chrome/bin/topic/1142433/inproduct?hl=";
56
57// static
58void PromoResourceService::RegisterPrefs(PrefService* local_state) {
59  local_state->RegisterIntegerPref(prefs::kNTPPromoVersion, 0);
60  local_state->RegisterStringPref(prefs::kNTPPromoLocale, std::string());
61}
62
63// static
64void 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
78bool 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
95PromoResourceService::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
109PromoResourceService::~PromoResourceService() { }
110
111void PromoResourceService::Init() {
112  ScheduleNotificationOnInit();
113}
114
115bool 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
124void PromoResourceService::Unpack(const DictionaryValue& parsed_json) {
125  UnpackLogoSignal(parsed_json);
126  UnpackPromoSignal(parsed_json);
127  UnpackWebStoreSignal(parsed_json);
128}
129
130void 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
151void 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
172int PromoResourceService::GetPromoServiceVersion() {
173  PrefService* local_state = g_browser_process->local_state();
174  return local_state->GetInteger(prefs::kNTPPromoVersion);
175}
176
177std::string PromoResourceService::GetPromoLocale() {
178  PrefService* local_state = g_browser_process->local_state();
179  return local_state->GetString(prefs::kNTPPromoLocale);
180}
181
182void 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
281void 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
347void 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
415namespace PromoResourceServiceUtil {
416
417bool 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