notification_promo.cc revision 5821806d5e7f356e8fa4b058a389a808ea183019
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/rand_util.h"
12#include "base/string_number_conversions.h"
13#include "base/string_util.h"
14#include "base/sys_info.h"
15#include "base/time.h"
16#include "base/values.h"
17#include "chrome/browser/prefs/pref_service.h"
18#include "chrome/browser/profiles/profile_impl.h"
19#include "chrome/browser/web_resource/promo_resource_service.h"
20#include "chrome/common/chrome_version_info.h"
21#include "chrome/common/net/url_util.h"
22#include "chrome/common/pref_names.h"
23#include "content/public/browser/user_metrics.h"
24#include "googleurl/src/gurl.h"
25
26#if defined(OS_ANDROID)
27#include "base/command_line.h"
28#include "chrome/common/chrome_switches.h"
29#endif  // defined(OS_ANDROID)
30
31using content::UserMetricsAction;
32
33namespace {
34
35const int kDefaultGroupSize = 100;
36
37const char promo_server_url[] = "https://clients3.google.com/crsignal/client";
38
39// The name of the preference that stores the promotion object.
40const char kPrefPromoObject[] = "promo";
41
42// Keys in the kPrefPromoObject dictionary; used only here.
43const char kPrefPromoText[] = "text";
44const char kPrefPromoPayload[] = "payload";
45const char kPrefPromoStart[] = "start";
46const char kPrefPromoEnd[] = "end";
47const char kPrefPromoNumGroups[] = "num_groups";
48const char kPrefPromoSegment[] = "segment";
49const char kPrefPromoIncrement[] = "increment";
50const char kPrefPromoIncrementFrequency[] = "increment_frequency";
51const char kPrefPromoIncrementMax[] = "increment_max";
52const char kPrefPromoMaxViews[] = "max_views";
53const char kPrefPromoGroup[] = "group";
54const char kPrefPromoViews[] = "views";
55const char kPrefPromoClosed[] = "closed";
56const char kPrefPromoGPlusRequired[] = "gplus_required";
57
58// Returns a string suitable for the Promo Server URL 'osname' value.
59std::string PlatformString() {
60#if defined(OS_WIN)
61  return "win";
62#elif defined(OS_IOS)
63  // TODO(noyau): add iOS-specific implementation
64  const bool isTablet = false;
65  return std::string("ios-") + (isTablet ? "tablet" : "phone");
66#elif defined(OS_MACOSX)
67  return "mac";
68#elif defined(OS_CHROMEOS)
69  return "chromeos";
70#elif defined(OS_LINUX)
71  return "linux";
72#elif defined(OS_ANDROID)
73  const bool isTablet =
74      CommandLine::ForCurrentProcess()->HasSwitch(switches::kTabletUI);
75  return std::string("android-") + (isTablet ? "tablet" : "phone");
76#else
77  return "none";
78#endif
79}
80
81// Returns a string suitable for the Promo Server URL 'dist' value.
82const 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
104struct PromoMapEntry {
105  NotificationPromo::PromoType promo_type;
106  const char* promo_type_str;
107};
108
109const 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};
115
116// Convert PromoType to appropriate string.
117const char* PromoTypeToString(NotificationPromo::PromoType promo_type) {
118  for (size_t i = 0; i < arraysize(kPromoMap); ++i) {
119    if (kPromoMap[i].promo_type == promo_type)
120      return kPromoMap[i].promo_type_str;
121  }
122  NOTREACHED();
123  return "";
124}
125
126// Deep-copies a node, replacing any "value" that is a key
127// into "strings" dictionary with its value from "strings".
128// E.g. for
129//   {promo_action_args:['MSG_SHORT']} + strings:{MSG_SHORT:'yes'}
130// it will return
131//   {promo_action_args:['yes']}
132// |node| - a value to be deep copied and resolved.
133// |strings| - a dictionary of strings to be used for resolution.
134// Returns a _new_ object that is a deep copy with replacements.
135// TODO(aruslan): http://crbug.com/144320 Consider moving it to values.cc/h.
136base::Value* DeepCopyAndResolveStrings(
137    const base::Value* node,
138    const base::DictionaryValue* strings) {
139  switch (node->GetType()) {
140    case base::Value::TYPE_LIST: {
141      const base::ListValue* list = static_cast<const base::ListValue*>(node);
142      base::ListValue* copy = new base::ListValue;
143      for (base::ListValue::const_iterator it = list->begin();
144           it != list->end(); ++it) {
145        base::Value* child_copy = DeepCopyAndResolveStrings(*it, strings);
146        copy->Append(child_copy);
147      }
148      return copy;
149    }
150
151    case Value::TYPE_DICTIONARY: {
152      const base::DictionaryValue* dict =
153          static_cast<const base::DictionaryValue*>(node);
154      base::DictionaryValue* copy = new base::DictionaryValue;
155      for (base::DictionaryValue::key_iterator it = dict->begin_keys();
156           it != dict->end_keys(); ++it) {
157        const base::Value* child = NULL;
158        bool rv = dict->GetWithoutPathExpansion(*it, &child);
159        DCHECK(rv);
160        base::Value* child_copy = DeepCopyAndResolveStrings(child, strings);
161        copy->SetWithoutPathExpansion(*it, child_copy);
162      }
163      return copy;
164    }
165
166    case Value::TYPE_STRING: {
167      const base::StringValue* str =
168          static_cast<const base::StringValue*>(node);
169      std::string value;
170      bool rv = str->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
184void AppendQueryParameter(GURL* url,
185                          const std::string& param,
186                          const std::string& value) {
187  *url = chrome_common_net::AppendQueryParameter(*url, param, value);
188}
189
190}  // namespace
191
192NotificationPromo::NotificationPromo(Profile* profile)
193    : profile_(profile),
194      prefs_(profile_->GetPrefs()),
195      promo_type_(NO_PROMO),
196      promo_payload_(new base::DictionaryValue()),
197      start_(0.0),
198      end_(0.0),
199      num_groups_(kDefaultGroupSize),
200      initial_segment_(0),
201      increment_(1),
202      time_slice_(0),
203      max_group_(0),
204      max_views_(0),
205      group_(0),
206      views_(0),
207      closed_(false),
208      gplus_required_(false),
209      new_notification_(false) {
210  DCHECK(profile);
211  DCHECK(prefs_);
212}
213
214NotificationPromo::~NotificationPromo() {}
215
216void NotificationPromo::InitFromJson(const DictionaryValue& json,
217                                     PromoType promo_type) {
218  promo_type_ = promo_type;
219  const ListValue* promo_list = NULL;
220  DVLOG(1) << "InitFromJson " << PromoTypeToString(promo_type_);
221  if (!json.GetList(PromoTypeToString(promo_type_), &promo_list))
222    return;
223
224  // No support for multiple promos yet. Only consider the first one.
225  const DictionaryValue* promo = NULL;
226  if (!promo_list->GetDictionary(0, &promo))
227    return;
228
229  // Date.
230  const ListValue* date_list = NULL;
231  if (promo->GetList("date", &date_list)) {
232    const DictionaryValue* date;
233    if (date_list->GetDictionary(0, &date)) {
234      std::string time_str;
235      base::Time time;
236      if (date->GetString("start", &time_str) &&
237          base::Time::FromString(time_str.c_str(), &time)) {
238        start_ = time.ToDoubleT();
239        DVLOG(1) << "start str=" << time_str
240                 << ", start_="<< base::DoubleToString(start_);
241      }
242      if (date->GetString("end", &time_str) &&
243          base::Time::FromString(time_str.c_str(), &time)) {
244        end_ = time.ToDoubleT();
245        DVLOG(1) << "end str =" << time_str
246                 << ", end_=" << base::DoubleToString(end_);
247      }
248    }
249  }
250
251  // Grouping.
252  const DictionaryValue* grouping = NULL;
253  if (promo->GetDictionary("grouping", &grouping)) {
254    grouping->GetInteger("buckets", &num_groups_);
255    grouping->GetInteger("segment", &initial_segment_);
256    grouping->GetInteger("increment", &increment_);
257    grouping->GetInteger("increment_frequency", &time_slice_);
258    grouping->GetInteger("increment_max", &max_group_);
259
260    DVLOG(1) << "num_groups_ = " << num_groups_
261             << ", initial_segment_ = " << initial_segment_
262             << ", increment_ = " << increment_
263             << ", time_slice_ = " << time_slice_
264             << ", max_group_ = " << max_group_;
265  }
266
267  // Strings.
268  const DictionaryValue* strings = NULL;
269  promo->GetDictionary("strings", &strings);
270
271  // Payload.
272  const DictionaryValue* payload = NULL;
273  if (promo->GetDictionary("payload", &payload)) {
274    payload->GetBoolean("gplus_required", &gplus_required_);
275    DVLOG(1) << "gplus_required_ = " << gplus_required_;
276
277    base::Value* ppcopy = DeepCopyAndResolveStrings(payload, strings);
278    DCHECK(ppcopy && ppcopy->IsType(base::Value::TYPE_DICTIONARY));
279    promo_payload_.reset(static_cast<base::DictionaryValue*>(ppcopy));
280  }
281
282  if (!promo_payload_->GetString("promo_message_short", &promo_text_) &&
283      strings) {
284    // For compatibility with the legacy desktop version,
285    // if no |payload.promo_message_short| is specified,
286    // the first string in |strings| is used.
287    DictionaryValue::Iterator iter(*strings);
288    iter.value().GetAsString(&promo_text_);
289  }
290  DVLOG(1) << "promo_text_=" << promo_text_;
291
292  promo->GetInteger("max_views", &max_views_);
293  DVLOG(1) << "max_views_ " << max_views_;
294
295  CheckForNewNotification();
296}
297
298void NotificationPromo::CheckForNewNotification() {
299  NotificationPromo old_promo(profile_);
300  old_promo.InitFromPrefs(promo_type_);
301  const double old_start = old_promo.start_;
302  const double old_end = old_promo.end_;
303  const std::string old_promo_text = old_promo.promo_text_;
304
305  new_notification_ =
306      old_start != start_ || old_end != end_ || old_promo_text != promo_text_;
307  if (new_notification_)
308    OnNewNotification();
309}
310
311void NotificationPromo::OnNewNotification() {
312  DVLOG(1) << "OnNewNotification";
313  // Create a new promo group.
314  group_ = base::RandInt(0, num_groups_ - 1);
315  WritePrefs();
316}
317
318// static
319void NotificationPromo::RegisterUserPrefs(PrefService* prefs) {
320  prefs->RegisterDictionaryPref(kPrefPromoObject,
321                                new base::DictionaryValue,
322                                PrefService::UNSYNCABLE_PREF);
323}
324
325void NotificationPromo::WritePrefs() {
326  base::DictionaryValue* ntp_promo = new base::DictionaryValue;
327  ntp_promo->SetString(kPrefPromoText, promo_text_);
328  ntp_promo->Set(kPrefPromoPayload, promo_payload_->DeepCopy());
329  ntp_promo->SetDouble(kPrefPromoStart, start_);
330  ntp_promo->SetDouble(kPrefPromoEnd, end_);
331
332  ntp_promo->SetInteger(kPrefPromoNumGroups, num_groups_);
333  ntp_promo->SetInteger(kPrefPromoSegment, initial_segment_);
334  ntp_promo->SetInteger(kPrefPromoIncrement, increment_);
335  ntp_promo->SetInteger(kPrefPromoIncrementFrequency, time_slice_);
336  ntp_promo->SetInteger(kPrefPromoIncrementMax, max_group_);
337
338  ntp_promo->SetInteger(kPrefPromoMaxViews, max_views_);
339
340  ntp_promo->SetInteger(kPrefPromoGroup, group_);
341  ntp_promo->SetInteger(kPrefPromoViews, views_);
342  ntp_promo->SetBoolean(kPrefPromoClosed, closed_);
343
344  ntp_promo->SetBoolean(kPrefPromoGPlusRequired, gplus_required_);
345
346  base::ListValue* promo_list = new base::ListValue;
347  promo_list->Set(0, ntp_promo);  // Only support 1 promo for now.
348
349  base::DictionaryValue promo_dict;
350  promo_dict.MergeDictionary(prefs_->GetDictionary(kPrefPromoObject));
351  promo_dict.Set(PromoTypeToString(promo_type_), promo_list);
352  prefs_->Set(kPrefPromoObject, promo_dict);
353  DVLOG(1) << "WritePrefs " << promo_dict;
354}
355
356void NotificationPromo::InitFromPrefs(PromoType promo_type) {
357  promo_type_ = promo_type;
358  const base::DictionaryValue* promo_dict =
359      prefs_->GetDictionary(kPrefPromoObject);
360  if (!promo_dict)
361    return;
362
363  const base::ListValue* promo_list = NULL;
364  promo_dict->GetList(PromoTypeToString(promo_type_), &promo_list);
365  if (!promo_list)
366    return;
367
368  const base::DictionaryValue* ntp_promo = NULL;
369  promo_list->GetDictionary(0, &ntp_promo);
370  if (!ntp_promo)
371    return;
372
373  ntp_promo->GetString(kPrefPromoText, &promo_text_);
374  const base::DictionaryValue* promo_payload = NULL;
375  if (ntp_promo->GetDictionary(kPrefPromoPayload, &promo_payload))
376    promo_payload_.reset(promo_payload->DeepCopy());
377
378  ntp_promo->GetDouble(kPrefPromoStart, &start_);
379  ntp_promo->GetDouble(kPrefPromoEnd, &end_);
380
381  ntp_promo->GetInteger(kPrefPromoNumGroups, &num_groups_);
382  ntp_promo->GetInteger(kPrefPromoSegment, &initial_segment_);
383  ntp_promo->GetInteger(kPrefPromoIncrement, &increment_);
384  ntp_promo->GetInteger(kPrefPromoIncrementFrequency, &time_slice_);
385  ntp_promo->GetInteger(kPrefPromoIncrementMax, &max_group_);
386
387  ntp_promo->GetInteger(kPrefPromoMaxViews, &max_views_);
388
389  ntp_promo->GetInteger(kPrefPromoGroup, &group_);
390  ntp_promo->GetInteger(kPrefPromoViews, &views_);
391  ntp_promo->GetBoolean(kPrefPromoClosed, &closed_);
392
393  ntp_promo->GetBoolean(kPrefPromoGPlusRequired, &gplus_required_);
394}
395
396bool NotificationPromo::CanShow() const {
397  return !closed_ &&
398         !promo_text_.empty() &&
399         !ExceedsMaxGroup() &&
400         !ExceedsMaxViews() &&
401         base::Time::FromDoubleT(StartTimeForGroup()) < base::Time::Now() &&
402         base::Time::FromDoubleT(EndTime()) > base::Time::Now() &&
403         IsGPlusRequired();
404}
405
406// static
407void NotificationPromo::HandleClosed(Profile* profile, PromoType promo_type) {
408  content::RecordAction(UserMetricsAction("NTPPromoClosed"));
409  NotificationPromo promo(profile);
410  promo.InitFromPrefs(promo_type);
411  if (!promo.closed_) {
412    promo.closed_ = true;
413    promo.WritePrefs();
414  }
415}
416
417// static
418bool NotificationPromo::HandleViewed(Profile* profile, PromoType promo_type) {
419  content::RecordAction(UserMetricsAction("NTPPromoShown"));
420  NotificationPromo promo(profile);
421  promo.InitFromPrefs(promo_type);
422  ++promo.views_;
423  promo.WritePrefs();
424  return promo.ExceedsMaxViews();
425}
426
427bool NotificationPromo::ExceedsMaxGroup() const {
428  return (max_group_ == 0) ? false : group_ >= max_group_;
429}
430
431bool NotificationPromo::ExceedsMaxViews() const {
432  return (max_views_ == 0) ? false : views_ >= max_views_;
433}
434
435bool NotificationPromo::IsGPlusRequired() const {
436  return !gplus_required_ || prefs_->GetBoolean(prefs::kIsGooglePlusUser);
437}
438
439// static
440GURL NotificationPromo::PromoServerURL() {
441  GURL url(promo_server_url);
442  AppendQueryParameter(&url, "dist", ChannelString());
443  AppendQueryParameter(&url, "osname", PlatformString());
444  AppendQueryParameter(&url, "branding", chrome::VersionInfo().Version());
445  AppendQueryParameter(&url, "osver", base::SysInfo::OperatingSystemVersion());
446  DVLOG(1) << "PromoServerURL=" << url.spec();
447  // Note that locale param is added by WebResourceService.
448  return url;
449}
450
451double NotificationPromo::StartTimeForGroup() const {
452  if (group_ < initial_segment_)
453    return start_;
454  return start_ +
455      std::ceil(static_cast<float>(group_ - initial_segment_ + 1) / increment_)
456      * time_slice_;
457}
458
459double NotificationPromo::EndTime() const {
460  return end_;
461}
462