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