• 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/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.
PlatformString()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.
ChannelString()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.
PromoTypeToString(NotificationPromo::PromoType promo_type)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.
DeepCopyAndResolveStrings(const base::Value * node,const base::DictionaryValue * strings)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 
AppendQueryParameter(GURL * url,const std::string & param,const std::string & value)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 
NotificationPromo()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 
~NotificationPromo()213 NotificationPromo::~NotificationPromo() {}
214 
InitFromJson(const base::DictionaryValue & json,PromoType promo_type)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 
CheckForNewNotification()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 
OnNewNotification()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
RegisterPrefs(PrefRegistrySimple * registry)318 void NotificationPromo::RegisterPrefs(PrefRegistrySimple* registry) {
319   registry->RegisterDictionaryPref(kPrefPromoObject);
320 }
321 
322 // static
RegisterProfilePrefs(user_prefs::PrefRegistrySyncable * registry)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
MigrateUserPrefs(PrefService * user_prefs)333 void NotificationPromo::MigrateUserPrefs(PrefService* user_prefs) {
334   user_prefs->ClearPref(kPrefPromoObject);
335 }
336 
WritePrefs()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 
InitFromPrefs(PromoType promo_type)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 
CheckAppLauncher() const408 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 
CanShow() const421 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
HandleClosed(PromoType promo_type)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
HandleViewed(PromoType promo_type)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 
ExceedsMaxGroup() const456 bool NotificationPromo::ExceedsMaxGroup() const {
457   return (max_group_ == 0) ? false : group_ >= max_group_;
458 }
459 
ExceedsMaxViews() const460 bool NotificationPromo::ExceedsMaxViews() const {
461   return (max_views_ == 0) ? false : views_ >= max_views_;
462 }
463 
ExceedsMaxSeconds() const464 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
PromoServerURL()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 
StartTimeForGroup() const485 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 
EndTime() const493 double NotificationPromo::EndTime() const {
494   return end_;
495 }
496