Home | History | Annotate | Download | only in web_resource
      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
     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
     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
     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 
     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 
    109 PromoResourceService::~PromoResourceService() { }
    110 
    111 void PromoResourceService::Init() {
    112   ScheduleNotificationOnInit();
    113 }
    114 
    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 
    124 void PromoResourceService::Unpack(const DictionaryValue& parsed_json) {
    125   UnpackLogoSignal(parsed_json);
    126   UnpackPromoSignal(parsed_json);
    127   UnpackWebStoreSignal(parsed_json);
    128 }
    129 
    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 
    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 
    172 int PromoResourceService::GetPromoServiceVersion() {
    173   PrefService* local_state = g_browser_process->local_state();
    174   return local_state->GetInteger(prefs::kNTPPromoVersion);
    175 }
    176 
    177 std::string PromoResourceService::GetPromoLocale() {
    178   PrefService* local_state = g_browser_process->local_state();
    179   return local_state->GetString(prefs::kNTPPromoLocale);
    180 }
    181 
    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 
    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 
    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 
    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