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