Home | History | Annotate | Download | only in web_resource
      1 // Copyright (c) 2012 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/notification_promo.h"
      6 
      7 #include <cmath>
      8 #include <vector>
      9 
     10 #include "base/bind.h"
     11 #include "base/prefs/pref_registry_simple.h"
     12 #include "base/prefs/pref_service.h"
     13 #include "base/rand_util.h"
     14 #include "base/strings/string_number_conversions.h"
     15 #include "base/strings/string_util.h"
     16 #include "base/sys_info.h"
     17 #include "base/threading/thread_restrictions.h"
     18 #include "base/time/time.h"
     19 #include "base/values.h"
     20 #include "chrome/browser/browser_process.h"
     21 #include "chrome/browser/web_resource/promo_resource_service.h"
     22 #include "chrome/common/chrome_version_info.h"
     23 #include "chrome/common/pref_names.h"
     24 #include "components/pref_registry/pref_registry_syncable.h"
     25 #include "content/public/browser/user_metrics.h"
     26 #include "net/base/url_util.h"
     27 #include "ui/base/device_form_factor.h"
     28 #include "url/gurl.h"
     29 
     30 using base::UserMetricsAction;
     31 
     32 namespace {
     33 
     34 const int kDefaultGroupSize = 100;
     35 
     36 const char promo_server_url[] = "https://clients3.google.com/crsignal/client";
     37 
     38 // The name of the preference that stores the promotion object.
     39 const char kPrefPromoObject[] = "promo";
     40 
     41 // Keys in the kPrefPromoObject dictionary; used only here.
     42 const char kPrefPromoText[] = "text";
     43 const char kPrefPromoPayload[] = "payload";
     44 const char kPrefPromoStart[] = "start";
     45 const char kPrefPromoEnd[] = "end";
     46 const char kPrefPromoNumGroups[] = "num_groups";
     47 const char kPrefPromoSegment[] = "segment";
     48 const char kPrefPromoIncrement[] = "increment";
     49 const char kPrefPromoIncrementFrequency[] = "increment_frequency";
     50 const char kPrefPromoIncrementMax[] = "increment_max";
     51 const char kPrefPromoMaxViews[] = "max_views";
     52 const char kPrefPromoMaxSeconds[] = "max_seconds";
     53 const char kPrefPromoFirstViewTime[] = "first_view_time";
     54 const char kPrefPromoGroup[] = "group";
     55 const char kPrefPromoViews[] = "views";
     56 const char kPrefPromoClosed[] = "closed";
     57 
     58 // Returns a string suitable for the Promo Server URL 'osname' value.
     59 std::string PlatformString() {
     60 #if defined(OS_WIN)
     61   return "win";
     62 #elif defined(OS_ANDROID)
     63   ui::DeviceFormFactor form_factor = ui::GetDeviceFormFactor();
     64   return std::string("android-") +
     65       (form_factor == ui::DEVICE_FORM_FACTOR_TABLET ? "tablet" : "phone");
     66 #elif defined(OS_IOS)
     67   ui::DeviceFormFactor form_factor = ui::GetDeviceFormFactor();
     68   return std::string("ios-") +
     69       (form_factor == ui::DEVICE_FORM_FACTOR_TABLET ? "tablet" : "phone");
     70 #elif defined(OS_MACOSX)
     71   return "mac";
     72 #elif defined(OS_CHROMEOS)
     73   return "chromeos";
     74 #elif defined(OS_LINUX)
     75   return "linux";
     76 #else
     77   return "none";
     78 #endif
     79 }
     80 
     81 // Returns a string suitable for the Promo Server URL 'dist' value.
     82 const char* ChannelString() {
     83 #if defined (OS_WIN)
     84   // GetChannel hits the registry on Windows. See http://crbug.com/70898.
     85   // TODO(achuith): Move NotificationPromo::PromoServerURL to the blocking pool.
     86   base::ThreadRestrictions::ScopedAllowIO allow_io;
     87 #endif
     88   const chrome::VersionInfo::Channel channel =
     89       chrome::VersionInfo::GetChannel();
     90   switch (channel) {
     91     case chrome::VersionInfo::CHANNEL_CANARY:
     92       return "canary";
     93     case chrome::VersionInfo::CHANNEL_DEV:
     94       return "dev";
     95     case chrome::VersionInfo::CHANNEL_BETA:
     96       return "beta";
     97     case chrome::VersionInfo::CHANNEL_STABLE:
     98       return "stable";
     99     default:
    100       return "none";
    101   }
    102 }
    103 
    104 struct PromoMapEntry {
    105   NotificationPromo::PromoType promo_type;
    106   const char* promo_type_str;
    107 };
    108 
    109 const PromoMapEntry kPromoMap[] = {
    110     { NotificationPromo::NO_PROMO, "" },
    111     { NotificationPromo::NTP_NOTIFICATION_PROMO, "ntp_notification_promo" },
    112     { NotificationPromo::NTP_BUBBLE_PROMO, "ntp_bubble_promo" },
    113     { NotificationPromo::MOBILE_NTP_SYNC_PROMO, "mobile_ntp_sync_promo" },
    114     { NotificationPromo::MOBILE_NTP_WHATS_NEW_PROMO,
    115         "mobile_ntp_whats_new_promo" },
    116 };
    117 
    118 // Convert PromoType to appropriate string.
    119 const char* PromoTypeToString(NotificationPromo::PromoType promo_type) {
    120   for (size_t i = 0; i < arraysize(kPromoMap); ++i) {
    121     if (kPromoMap[i].promo_type == promo_type)
    122       return kPromoMap[i].promo_type_str;
    123   }
    124   NOTREACHED();
    125   return "";
    126 }
    127 
    128 // Deep-copies a node, replacing any "value" that is a key
    129 // into "strings" dictionary with its value from "strings".
    130 // E.g. for
    131 //   {promo_action_args:['MSG_SHORT']} + strings:{MSG_SHORT:'yes'}
    132 // it will return
    133 //   {promo_action_args:['yes']}
    134 // |node| - a value to be deep copied and resolved.
    135 // |strings| - a dictionary of strings to be used for resolution.
    136 // Returns a _new_ object that is a deep copy with replacements.
    137 // TODO(aruslan): http://crbug.com/144320 Consider moving it to values.cc/h.
    138 base::Value* DeepCopyAndResolveStrings(
    139     const base::Value* node,
    140     const base::DictionaryValue* strings) {
    141   switch (node->GetType()) {
    142     case base::Value::TYPE_LIST: {
    143       const base::ListValue* list = static_cast<const base::ListValue*>(node);
    144       base::ListValue* copy = new base::ListValue;
    145       for (base::ListValue::const_iterator it = list->begin();
    146            it != list->end();
    147            ++it) {
    148         base::Value* child_copy = DeepCopyAndResolveStrings(*it, strings);
    149         copy->Append(child_copy);
    150       }
    151       return copy;
    152     }
    153 
    154     case base::Value::TYPE_DICTIONARY: {
    155       const base::DictionaryValue* dict =
    156           static_cast<const base::DictionaryValue*>(node);
    157       base::DictionaryValue* copy = new base::DictionaryValue;
    158       for (base::DictionaryValue::Iterator it(*dict);
    159            !it.IsAtEnd();
    160            it.Advance()) {
    161         base::Value* child_copy = DeepCopyAndResolveStrings(&it.value(),
    162                                                             strings);
    163         copy->SetWithoutPathExpansion(it.key(), child_copy);
    164       }
    165       return copy;
    166     }
    167 
    168     case base::Value::TYPE_STRING: {
    169       std::string value;
    170       bool rv = node->GetAsString(&value);
    171       DCHECK(rv);
    172       std::string actual_value;
    173       if (!strings || !strings->GetString(value, &actual_value))
    174         actual_value = value;
    175       return new base::StringValue(actual_value);
    176     }
    177 
    178     default:
    179       // For everything else, just make a copy.
    180       return node->DeepCopy();
    181   }
    182 }
    183 
    184 void AppendQueryParameter(GURL* url,
    185                           const std::string& param,
    186                           const std::string& value) {
    187   *url = net::AppendQueryParameter(*url, param, value);
    188 }
    189 
    190 }  // namespace
    191 
    192 NotificationPromo::NotificationPromo()
    193     : prefs_(g_browser_process->local_state()),
    194       promo_type_(NO_PROMO),
    195       promo_payload_(new base::DictionaryValue()),
    196       start_(0.0),
    197       end_(0.0),
    198       num_groups_(kDefaultGroupSize),
    199       initial_segment_(0),
    200       increment_(1),
    201       time_slice_(0),
    202       max_group_(0),
    203       max_views_(0),
    204       max_seconds_(0),
    205       first_view_time_(0),
    206       group_(0),
    207       views_(0),
    208       closed_(false),
    209       new_notification_(false) {
    210   DCHECK(prefs_);
    211 }
    212 
    213 NotificationPromo::~NotificationPromo() {}
    214 
    215 void NotificationPromo::InitFromJson(const base::DictionaryValue& json,
    216                                      PromoType promo_type) {
    217   promo_type_ = promo_type;
    218   const base::ListValue* promo_list = NULL;
    219   DVLOG(1) << "InitFromJson " << PromoTypeToString(promo_type_);
    220   if (!json.GetList(PromoTypeToString(promo_type_), &promo_list))
    221     return;
    222 
    223   // No support for multiple promos yet. Only consider the first one.
    224   const base::DictionaryValue* promo = NULL;
    225   if (!promo_list->GetDictionary(0, &promo))
    226     return;
    227 
    228   // Date.
    229   const base::ListValue* date_list = NULL;
    230   if (promo->GetList("date", &date_list)) {
    231     const base::DictionaryValue* date;
    232     if (date_list->GetDictionary(0, &date)) {
    233       std::string time_str;
    234       base::Time time;
    235       if (date->GetString("start", &time_str) &&
    236           base::Time::FromString(time_str.c_str(), &time)) {
    237         start_ = time.ToDoubleT();
    238         DVLOG(1) << "start str=" << time_str
    239                  << ", start_="<< base::DoubleToString(start_);
    240       }
    241       if (date->GetString("end", &time_str) &&
    242           base::Time::FromString(time_str.c_str(), &time)) {
    243         end_ = time.ToDoubleT();
    244         DVLOG(1) << "end str =" << time_str
    245                  << ", end_=" << base::DoubleToString(end_);
    246       }
    247     }
    248   }
    249 
    250   // Grouping.
    251   const base::DictionaryValue* grouping = NULL;
    252   if (promo->GetDictionary("grouping", &grouping)) {
    253     grouping->GetInteger("buckets", &num_groups_);
    254     grouping->GetInteger("segment", &initial_segment_);
    255     grouping->GetInteger("increment", &increment_);
    256     grouping->GetInteger("increment_frequency", &time_slice_);
    257     grouping->GetInteger("increment_max", &max_group_);
    258 
    259     DVLOG(1) << "num_groups_ = " << num_groups_
    260              << ", initial_segment_ = " << initial_segment_
    261              << ", increment_ = " << increment_
    262              << ", time_slice_ = " << time_slice_
    263              << ", max_group_ = " << max_group_;
    264   }
    265 
    266   // Strings.
    267   const base::DictionaryValue* strings = NULL;
    268   promo->GetDictionary("strings", &strings);
    269 
    270   // Payload.
    271   const base::DictionaryValue* payload = NULL;
    272   if (promo->GetDictionary("payload", &payload)) {
    273     base::Value* ppcopy = DeepCopyAndResolveStrings(payload, strings);
    274     DCHECK(ppcopy && ppcopy->IsType(base::Value::TYPE_DICTIONARY));
    275     promo_payload_.reset(static_cast<base::DictionaryValue*>(ppcopy));
    276   }
    277 
    278   if (!promo_payload_->GetString("promo_message_short", &promo_text_) &&
    279       strings) {
    280     // For compatibility with the legacy desktop version,
    281     // if no |payload.promo_message_short| is specified,
    282     // the first string in |strings| is used.
    283     base::DictionaryValue::Iterator iter(*strings);
    284     iter.value().GetAsString(&promo_text_);
    285   }
    286   DVLOG(1) << "promo_text_=" << promo_text_;
    287 
    288   promo->GetInteger("max_views", &max_views_);
    289   DVLOG(1) << "max_views_ " << max_views_;
    290 
    291   promo->GetInteger("max_seconds", &max_seconds_);
    292   DVLOG(1) << "max_seconds_ " << max_seconds_;
    293 
    294   CheckForNewNotification();
    295 }
    296 
    297 void NotificationPromo::CheckForNewNotification() {
    298   NotificationPromo old_promo;
    299   old_promo.InitFromPrefs(promo_type_);
    300   const double old_start = old_promo.start_;
    301   const double old_end = old_promo.end_;
    302   const std::string old_promo_text = old_promo.promo_text_;
    303 
    304   new_notification_ =
    305       old_start != start_ || old_end != end_ || old_promo_text != promo_text_;
    306   if (new_notification_)
    307     OnNewNotification();
    308 }
    309 
    310 void NotificationPromo::OnNewNotification() {
    311   DVLOG(1) << "OnNewNotification";
    312   // Create a new promo group.
    313   group_ = base::RandInt(0, num_groups_ - 1);
    314   WritePrefs();
    315 }
    316 
    317 // static
    318 void NotificationPromo::RegisterPrefs(PrefRegistrySimple* registry) {
    319   registry->RegisterDictionaryPref(kPrefPromoObject);
    320 }
    321 
    322 // static
    323 void NotificationPromo::RegisterProfilePrefs(
    324     user_prefs::PrefRegistrySyncable* registry) {
    325   // TODO(dbeam): Registered only for migration. Remove in M28 when
    326   // we're reasonably sure all prefs are gone.
    327   // http://crbug.com/168887
    328   registry->RegisterDictionaryPref(
    329       kPrefPromoObject, user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF);
    330 }
    331 
    332 // static
    333 void NotificationPromo::MigrateUserPrefs(PrefService* user_prefs) {
    334   user_prefs->ClearPref(kPrefPromoObject);
    335 }
    336 
    337 void NotificationPromo::WritePrefs() {
    338   base::DictionaryValue* ntp_promo = new base::DictionaryValue;
    339   ntp_promo->SetString(kPrefPromoText, promo_text_);
    340   ntp_promo->Set(kPrefPromoPayload, promo_payload_->DeepCopy());
    341   ntp_promo->SetDouble(kPrefPromoStart, start_);
    342   ntp_promo->SetDouble(kPrefPromoEnd, end_);
    343 
    344   ntp_promo->SetInteger(kPrefPromoNumGroups, num_groups_);
    345   ntp_promo->SetInteger(kPrefPromoSegment, initial_segment_);
    346   ntp_promo->SetInteger(kPrefPromoIncrement, increment_);
    347   ntp_promo->SetInteger(kPrefPromoIncrementFrequency, time_slice_);
    348   ntp_promo->SetInteger(kPrefPromoIncrementMax, max_group_);
    349 
    350   ntp_promo->SetInteger(kPrefPromoMaxViews, max_views_);
    351   ntp_promo->SetInteger(kPrefPromoMaxSeconds, max_seconds_);
    352   ntp_promo->SetDouble(kPrefPromoFirstViewTime, first_view_time_);
    353 
    354   ntp_promo->SetInteger(kPrefPromoGroup, group_);
    355   ntp_promo->SetInteger(kPrefPromoViews, views_);
    356   ntp_promo->SetBoolean(kPrefPromoClosed, closed_);
    357 
    358   base::ListValue* promo_list = new base::ListValue;
    359   promo_list->Set(0, ntp_promo);  // Only support 1 promo for now.
    360 
    361   base::DictionaryValue promo_dict;
    362   promo_dict.MergeDictionary(prefs_->GetDictionary(kPrefPromoObject));
    363   promo_dict.Set(PromoTypeToString(promo_type_), promo_list);
    364   prefs_->Set(kPrefPromoObject, promo_dict);
    365   DVLOG(1) << "WritePrefs " << promo_dict;
    366 }
    367 
    368 void NotificationPromo::InitFromPrefs(PromoType promo_type) {
    369   promo_type_ = promo_type;
    370   const base::DictionaryValue* promo_dict =
    371       prefs_->GetDictionary(kPrefPromoObject);
    372   if (!promo_dict)
    373     return;
    374 
    375   const base::ListValue* promo_list = NULL;
    376   promo_dict->GetList(PromoTypeToString(promo_type_), &promo_list);
    377   if (!promo_list)
    378     return;
    379 
    380   const base::DictionaryValue* ntp_promo = NULL;
    381   promo_list->GetDictionary(0, &ntp_promo);
    382   if (!ntp_promo)
    383     return;
    384 
    385   ntp_promo->GetString(kPrefPromoText, &promo_text_);
    386   const base::DictionaryValue* promo_payload = NULL;
    387   if (ntp_promo->GetDictionary(kPrefPromoPayload, &promo_payload))
    388     promo_payload_.reset(promo_payload->DeepCopy());
    389 
    390   ntp_promo->GetDouble(kPrefPromoStart, &start_);
    391   ntp_promo->GetDouble(kPrefPromoEnd, &end_);
    392 
    393   ntp_promo->GetInteger(kPrefPromoNumGroups, &num_groups_);
    394   ntp_promo->GetInteger(kPrefPromoSegment, &initial_segment_);
    395   ntp_promo->GetInteger(kPrefPromoIncrement, &increment_);
    396   ntp_promo->GetInteger(kPrefPromoIncrementFrequency, &time_slice_);
    397   ntp_promo->GetInteger(kPrefPromoIncrementMax, &max_group_);
    398 
    399   ntp_promo->GetInteger(kPrefPromoMaxViews, &max_views_);
    400   ntp_promo->GetInteger(kPrefPromoMaxSeconds, &max_seconds_);
    401   ntp_promo->GetDouble(kPrefPromoFirstViewTime, &first_view_time_);
    402 
    403   ntp_promo->GetInteger(kPrefPromoGroup, &group_);
    404   ntp_promo->GetInteger(kPrefPromoViews, &views_);
    405   ntp_promo->GetBoolean(kPrefPromoClosed, &closed_);
    406 }
    407 
    408 bool NotificationPromo::CheckAppLauncher() const {
    409 #if !defined(ENABLE_APP_LIST)
    410   return true;
    411 #else
    412   bool is_app_launcher_promo = false;
    413   if (!promo_payload_->GetBoolean("is_app_launcher_promo",
    414                                   &is_app_launcher_promo))
    415     return true;
    416   return !is_app_launcher_promo ||
    417          !prefs_->GetBoolean(prefs::kAppLauncherIsEnabled);
    418 #endif  // !defined(ENABLE_APP_LIST)
    419 }
    420 
    421 bool NotificationPromo::CanShow() const {
    422   return !closed_ &&
    423          !promo_text_.empty() &&
    424          !ExceedsMaxGroup() &&
    425          !ExceedsMaxViews() &&
    426          !ExceedsMaxSeconds() &&
    427          CheckAppLauncher() &&
    428          base::Time::FromDoubleT(StartTimeForGroup()) < base::Time::Now() &&
    429          base::Time::FromDoubleT(EndTime()) > base::Time::Now();
    430 }
    431 
    432 // static
    433 void NotificationPromo::HandleClosed(PromoType promo_type) {
    434   content::RecordAction(UserMetricsAction("NTPPromoClosed"));
    435   NotificationPromo promo;
    436   promo.InitFromPrefs(promo_type);
    437   if (!promo.closed_) {
    438     promo.closed_ = true;
    439     promo.WritePrefs();
    440   }
    441 }
    442 
    443 // static
    444 bool NotificationPromo::HandleViewed(PromoType promo_type) {
    445   content::RecordAction(UserMetricsAction("NTPPromoShown"));
    446   NotificationPromo promo;
    447   promo.InitFromPrefs(promo_type);
    448   ++promo.views_;
    449   if (promo.first_view_time_ == 0) {
    450     promo.first_view_time_ = base::Time::Now().ToDoubleT();
    451   }
    452   promo.WritePrefs();
    453   return promo.ExceedsMaxViews() || promo.ExceedsMaxSeconds();
    454 }
    455 
    456 bool NotificationPromo::ExceedsMaxGroup() const {
    457   return (max_group_ == 0) ? false : group_ >= max_group_;
    458 }
    459 
    460 bool NotificationPromo::ExceedsMaxViews() const {
    461   return (max_views_ == 0) ? false : views_ >= max_views_;
    462 }
    463 
    464 bool NotificationPromo::ExceedsMaxSeconds() const {
    465   if (max_seconds_ == 0 || first_view_time_ == 0)
    466     return false;
    467 
    468   const base::Time last_view_time = base::Time::FromDoubleT(first_view_time_) +
    469                                     base::TimeDelta::FromSeconds(max_seconds_);
    470   return last_view_time < base::Time::Now();
    471 }
    472 
    473 // static
    474 GURL NotificationPromo::PromoServerURL() {
    475   GURL url(promo_server_url);
    476   AppendQueryParameter(&url, "dist", ChannelString());
    477   AppendQueryParameter(&url, "osname", PlatformString());
    478   AppendQueryParameter(&url, "branding", chrome::VersionInfo().Version());
    479   AppendQueryParameter(&url, "osver", base::SysInfo::OperatingSystemVersion());
    480   DVLOG(1) << "PromoServerURL=" << url.spec();
    481   // Note that locale param is added by WebResourceService.
    482   return url;
    483 }
    484 
    485 double NotificationPromo::StartTimeForGroup() const {
    486   if (group_ < initial_segment_)
    487     return start_;
    488   return start_ +
    489       std::ceil(static_cast<float>(group_ - initial_segment_ + 1) / increment_)
    490       * time_slice_;
    491 }
    492 
    493 double NotificationPromo::EndTime() const {
    494   return end_;
    495 }
    496