// Copyright (c) 2012 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/notification_promo.h" #include #include #include "base/bind.h" #include "base/rand_util.h" #include "base/string_number_conversions.h" #include "base/string_util.h" #include "base/sys_info.h" #include "base/threading/thread_restrictions.h" #include "base/time.h" #include "base/values.h" #include "chrome/browser/browser_process.h" #include "chrome/browser/prefs/pref_service.h" #include "chrome/browser/prefs/pref_service_simple.h" #include "chrome/browser/prefs/pref_service_syncable.h" #include "chrome/browser/web_resource/promo_resource_service.h" #include "chrome/common/chrome_version_info.h" #include "chrome/common/net/url_util.h" #include "chrome/common/pref_names.h" #include "content/public/browser/user_metrics.h" #include "googleurl/src/gurl.h" #if defined(OS_ANDROID) #include "base/command_line.h" #include "chrome/common/chrome_switches.h" #endif // defined(OS_ANDROID) using content::UserMetricsAction; namespace { const int kDefaultGroupSize = 100; const char promo_server_url[] = "https://clients3.google.com/crsignal/client"; // The name of the preference that stores the promotion object. const char kPrefPromoObject[] = "promo"; // Keys in the kPrefPromoObject dictionary; used only here. const char kPrefPromoText[] = "text"; const char kPrefPromoPayload[] = "payload"; const char kPrefPromoStart[] = "start"; const char kPrefPromoEnd[] = "end"; const char kPrefPromoNumGroups[] = "num_groups"; const char kPrefPromoSegment[] = "segment"; const char kPrefPromoIncrement[] = "increment"; const char kPrefPromoIncrementFrequency[] = "increment_frequency"; const char kPrefPromoIncrementMax[] = "increment_max"; const char kPrefPromoMaxViews[] = "max_views"; const char kPrefPromoGroup[] = "group"; const char kPrefPromoViews[] = "views"; const char kPrefPromoClosed[] = "closed"; // Returns a string suitable for the Promo Server URL 'osname' value. std::string PlatformString() { #if defined(OS_WIN) return "win"; #elif defined(OS_IOS) // TODO(noyau): add iOS-specific implementation const bool isTablet = false; return std::string("ios-") + (isTablet ? "tablet" : "phone"); #elif defined(OS_MACOSX) return "mac"; #elif defined(OS_CHROMEOS) return "chromeos"; #elif defined(OS_LINUX) return "linux"; #elif defined(OS_ANDROID) const bool isTablet = CommandLine::ForCurrentProcess()->HasSwitch(switches::kTabletUI); return std::string("android-") + (isTablet ? "tablet" : "phone"); #else return "none"; #endif } // Returns a string suitable for the Promo Server URL 'dist' value. const char* ChannelString() { #if defined (OS_WIN) // GetChannel hits the registry on Windows. See http://crbug.com/70898. // TODO(achuith): Move NotificationPromo::PromoServerURL to the blocking pool. base::ThreadRestrictions::ScopedAllowIO allow_io; #endif const chrome::VersionInfo::Channel channel = chrome::VersionInfo::GetChannel(); switch (channel) { case chrome::VersionInfo::CHANNEL_CANARY: return "canary"; case chrome::VersionInfo::CHANNEL_DEV: return "dev"; case chrome::VersionInfo::CHANNEL_BETA: return "beta"; case chrome::VersionInfo::CHANNEL_STABLE: return "stable"; default: return "none"; } } struct PromoMapEntry { NotificationPromo::PromoType promo_type; const char* promo_type_str; }; const PromoMapEntry kPromoMap[] = { { NotificationPromo::NO_PROMO, "" }, { NotificationPromo::NTP_NOTIFICATION_PROMO, "ntp_notification_promo" }, { NotificationPromo::NTP_BUBBLE_PROMO, "ntp_bubble_promo" }, { NotificationPromo::MOBILE_NTP_SYNC_PROMO, "mobile_ntp_sync_promo" }, }; // Convert PromoType to appropriate string. const char* PromoTypeToString(NotificationPromo::PromoType promo_type) { for (size_t i = 0; i < arraysize(kPromoMap); ++i) { if (kPromoMap[i].promo_type == promo_type) return kPromoMap[i].promo_type_str; } NOTREACHED(); return ""; } // Deep-copies a node, replacing any "value" that is a key // into "strings" dictionary with its value from "strings". // E.g. for // {promo_action_args:['MSG_SHORT']} + strings:{MSG_SHORT:'yes'} // it will return // {promo_action_args:['yes']} // |node| - a value to be deep copied and resolved. // |strings| - a dictionary of strings to be used for resolution. // Returns a _new_ object that is a deep copy with replacements. // TODO(aruslan): http://crbug.com/144320 Consider moving it to values.cc/h. base::Value* DeepCopyAndResolveStrings( const base::Value* node, const base::DictionaryValue* strings) { switch (node->GetType()) { case base::Value::TYPE_LIST: { const base::ListValue* list = static_cast(node); base::ListValue* copy = new base::ListValue; for (base::ListValue::const_iterator it = list->begin(); it != list->end(); ++it) { base::Value* child_copy = DeepCopyAndResolveStrings(*it, strings); copy->Append(child_copy); } return copy; } case Value::TYPE_DICTIONARY: { const base::DictionaryValue* dict = static_cast(node); base::DictionaryValue* copy = new base::DictionaryValue; for (base::DictionaryValue::key_iterator it = dict->begin_keys(); it != dict->end_keys(); ++it) { const base::Value* child = NULL; bool rv = dict->GetWithoutPathExpansion(*it, &child); DCHECK(rv); base::Value* child_copy = DeepCopyAndResolveStrings(child, strings); copy->SetWithoutPathExpansion(*it, child_copy); } return copy; } case Value::TYPE_STRING: { std::string value; bool rv = node->GetAsString(&value); DCHECK(rv); std::string actual_value; if (!strings || !strings->GetString(value, &actual_value)) actual_value = value; return new base::StringValue(actual_value); } default: // For everything else, just make a copy. return node->DeepCopy(); } } void AppendQueryParameter(GURL* url, const std::string& param, const std::string& value) { *url = chrome_common_net::AppendQueryParameter(*url, param, value); } } // namespace NotificationPromo::NotificationPromo() : prefs_(g_browser_process->local_state()), promo_type_(NO_PROMO), promo_payload_(new base::DictionaryValue()), start_(0.0), end_(0.0), num_groups_(kDefaultGroupSize), initial_segment_(0), increment_(1), time_slice_(0), max_group_(0), max_views_(0), group_(0), views_(0), closed_(false), new_notification_(false) { DCHECK(prefs_); } NotificationPromo::~NotificationPromo() {} void NotificationPromo::InitFromJson(const DictionaryValue& json, PromoType promo_type) { promo_type_ = promo_type; const ListValue* promo_list = NULL; DVLOG(1) << "InitFromJson " << PromoTypeToString(promo_type_); if (!json.GetList(PromoTypeToString(promo_type_), &promo_list)) return; // No support for multiple promos yet. Only consider the first one. const DictionaryValue* promo = NULL; if (!promo_list->GetDictionary(0, &promo)) return; // Date. const ListValue* date_list = NULL; if (promo->GetList("date", &date_list)) { const DictionaryValue* date; if (date_list->GetDictionary(0, &date)) { std::string time_str; base::Time time; if (date->GetString("start", &time_str) && base::Time::FromString(time_str.c_str(), &time)) { start_ = time.ToDoubleT(); DVLOG(1) << "start str=" << time_str << ", start_="<< base::DoubleToString(start_); } if (date->GetString("end", &time_str) && base::Time::FromString(time_str.c_str(), &time)) { end_ = time.ToDoubleT(); DVLOG(1) << "end str =" << time_str << ", end_=" << base::DoubleToString(end_); } } } // Grouping. const DictionaryValue* grouping = NULL; if (promo->GetDictionary("grouping", &grouping)) { grouping->GetInteger("buckets", &num_groups_); grouping->GetInteger("segment", &initial_segment_); grouping->GetInteger("increment", &increment_); grouping->GetInteger("increment_frequency", &time_slice_); grouping->GetInteger("increment_max", &max_group_); DVLOG(1) << "num_groups_ = " << num_groups_ << ", initial_segment_ = " << initial_segment_ << ", increment_ = " << increment_ << ", time_slice_ = " << time_slice_ << ", max_group_ = " << max_group_; } // Strings. const DictionaryValue* strings = NULL; promo->GetDictionary("strings", &strings); // Payload. const DictionaryValue* payload = NULL; if (promo->GetDictionary("payload", &payload)) { base::Value* ppcopy = DeepCopyAndResolveStrings(payload, strings); DCHECK(ppcopy && ppcopy->IsType(base::Value::TYPE_DICTIONARY)); promo_payload_.reset(static_cast(ppcopy)); } if (!promo_payload_->GetString("promo_message_short", &promo_text_) && strings) { // For compatibility with the legacy desktop version, // if no |payload.promo_message_short| is specified, // the first string in |strings| is used. DictionaryValue::Iterator iter(*strings); iter.value().GetAsString(&promo_text_); } DVLOG(1) << "promo_text_=" << promo_text_; promo->GetInteger("max_views", &max_views_); DVLOG(1) << "max_views_ " << max_views_; CheckForNewNotification(); } void NotificationPromo::CheckForNewNotification() { NotificationPromo old_promo; old_promo.InitFromPrefs(promo_type_); const double old_start = old_promo.start_; const double old_end = old_promo.end_; const std::string old_promo_text = old_promo.promo_text_; new_notification_ = old_start != start_ || old_end != end_ || old_promo_text != promo_text_; if (new_notification_) OnNewNotification(); } void NotificationPromo::OnNewNotification() { DVLOG(1) << "OnNewNotification"; // Create a new promo group. group_ = base::RandInt(0, num_groups_ - 1); WritePrefs(); } // static void NotificationPromo::RegisterPrefs(PrefServiceSimple* local_state) { local_state->RegisterDictionaryPref(kPrefPromoObject); } // static void NotificationPromo::RegisterUserPrefs(PrefServiceSyncable* prefs) { // TODO(dbeam): Remove in M28 when we're reasonably sure all prefs are gone. // http://crbug.com/168887 prefs->RegisterDictionaryPref(kPrefPromoObject, PrefServiceSyncable::UNSYNCABLE_PREF); prefs->ClearPref(kPrefPromoObject); } void NotificationPromo::WritePrefs() { base::DictionaryValue* ntp_promo = new base::DictionaryValue; ntp_promo->SetString(kPrefPromoText, promo_text_); ntp_promo->Set(kPrefPromoPayload, promo_payload_->DeepCopy()); ntp_promo->SetDouble(kPrefPromoStart, start_); ntp_promo->SetDouble(kPrefPromoEnd, end_); ntp_promo->SetInteger(kPrefPromoNumGroups, num_groups_); ntp_promo->SetInteger(kPrefPromoSegment, initial_segment_); ntp_promo->SetInteger(kPrefPromoIncrement, increment_); ntp_promo->SetInteger(kPrefPromoIncrementFrequency, time_slice_); ntp_promo->SetInteger(kPrefPromoIncrementMax, max_group_); ntp_promo->SetInteger(kPrefPromoMaxViews, max_views_); ntp_promo->SetInteger(kPrefPromoGroup, group_); ntp_promo->SetInteger(kPrefPromoViews, views_); ntp_promo->SetBoolean(kPrefPromoClosed, closed_); base::ListValue* promo_list = new base::ListValue; promo_list->Set(0, ntp_promo); // Only support 1 promo for now. base::DictionaryValue promo_dict; promo_dict.MergeDictionary(prefs_->GetDictionary(kPrefPromoObject)); promo_dict.Set(PromoTypeToString(promo_type_), promo_list); prefs_->Set(kPrefPromoObject, promo_dict); DVLOG(1) << "WritePrefs " << promo_dict; } void NotificationPromo::InitFromPrefs(PromoType promo_type) { promo_type_ = promo_type; const base::DictionaryValue* promo_dict = prefs_->GetDictionary(kPrefPromoObject); if (!promo_dict) return; const base::ListValue* promo_list = NULL; promo_dict->GetList(PromoTypeToString(promo_type_), &promo_list); if (!promo_list) return; const base::DictionaryValue* ntp_promo = NULL; promo_list->GetDictionary(0, &ntp_promo); if (!ntp_promo) return; ntp_promo->GetString(kPrefPromoText, &promo_text_); const base::DictionaryValue* promo_payload = NULL; if (ntp_promo->GetDictionary(kPrefPromoPayload, &promo_payload)) promo_payload_.reset(promo_payload->DeepCopy()); ntp_promo->GetDouble(kPrefPromoStart, &start_); ntp_promo->GetDouble(kPrefPromoEnd, &end_); ntp_promo->GetInteger(kPrefPromoNumGroups, &num_groups_); ntp_promo->GetInteger(kPrefPromoSegment, &initial_segment_); ntp_promo->GetInteger(kPrefPromoIncrement, &increment_); ntp_promo->GetInteger(kPrefPromoIncrementFrequency, &time_slice_); ntp_promo->GetInteger(kPrefPromoIncrementMax, &max_group_); ntp_promo->GetInteger(kPrefPromoMaxViews, &max_views_); ntp_promo->GetInteger(kPrefPromoGroup, &group_); ntp_promo->GetInteger(kPrefPromoViews, &views_); ntp_promo->GetBoolean(kPrefPromoClosed, &closed_); } bool NotificationPromo::CanShow() const { return !closed_ && !promo_text_.empty() && !ExceedsMaxGroup() && !ExceedsMaxViews() && base::Time::FromDoubleT(StartTimeForGroup()) < base::Time::Now() && base::Time::FromDoubleT(EndTime()) > base::Time::Now(); } // static void NotificationPromo::HandleClosed(PromoType promo_type) { content::RecordAction(UserMetricsAction("NTPPromoClosed")); NotificationPromo promo; promo.InitFromPrefs(promo_type); if (!promo.closed_) { promo.closed_ = true; promo.WritePrefs(); } } // static bool NotificationPromo::HandleViewed(PromoType promo_type) { content::RecordAction(UserMetricsAction("NTPPromoShown")); NotificationPromo promo; promo.InitFromPrefs(promo_type); ++promo.views_; promo.WritePrefs(); return promo.ExceedsMaxViews(); } bool NotificationPromo::ExceedsMaxGroup() const { return (max_group_ == 0) ? false : group_ >= max_group_; } bool NotificationPromo::ExceedsMaxViews() const { return (max_views_ == 0) ? false : views_ >= max_views_; } // static GURL NotificationPromo::PromoServerURL() { GURL url(promo_server_url); AppendQueryParameter(&url, "dist", ChannelString()); AppendQueryParameter(&url, "osname", PlatformString()); AppendQueryParameter(&url, "branding", chrome::VersionInfo().Version()); AppendQueryParameter(&url, "osver", base::SysInfo::OperatingSystemVersion()); DVLOG(1) << "PromoServerURL=" << url.spec(); // Note that locale param is added by WebResourceService. return url; } double NotificationPromo::StartTimeForGroup() const { if (group_ < initial_segment_) return start_; return start_ + std::ceil(static_cast(group_ - initial_segment_ + 1) / increment_) * time_slice_; } double NotificationPromo::EndTime() const { return end_; }