普通文本  |  442行  |  16.73 KB

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