notification_promo.cc revision 116680a4aac90f2aa7413d9095a592090648e557
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
30using base::UserMetricsAction;
31
32namespace {
33
34const int kDefaultGroupSize = 100;
35
36const char promo_server_url[] = "https://clients3.google.com/crsignal/client";
37
38// The name of the preference that stores the promotion object.
39const char kPrefPromoObject[] = "promo";
40
41// Keys in the kPrefPromoObject dictionary; used only here.
42const char kPrefPromoText[] = "text";
43const char kPrefPromoPayload[] = "payload";
44const char kPrefPromoStart[] = "start";
45const char kPrefPromoEnd[] = "end";
46const char kPrefPromoNumGroups[] = "num_groups";
47const char kPrefPromoSegment[] = "segment";
48const char kPrefPromoIncrement[] = "increment";
49const char kPrefPromoIncrementFrequency[] = "increment_frequency";
50const char kPrefPromoIncrementMax[] = "increment_max";
51const char kPrefPromoMaxViews[] = "max_views";
52const char kPrefPromoGroup[] = "group";
53const char kPrefPromoViews[] = "views";
54const char kPrefPromoClosed[] = "closed";
55
56// Returns a string suitable for the Promo Server URL 'osname' value.
57std::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.
80const 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
102struct PromoMapEntry {
103  NotificationPromo::PromoType promo_type;
104  const char* promo_type_str;
105};
106
107const 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    { NotificationPromo::MOBILE_NTP_WHATS_NEW_PROMO,
113        "mobile_ntp_whats_new_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();
145           ++it) {
146        base::Value* child_copy = DeepCopyAndResolveStrings(*it, strings);
147        copy->Append(child_copy);
148      }
149      return copy;
150    }
151
152    case base::Value::TYPE_DICTIONARY: {
153      const base::DictionaryValue* dict =
154          static_cast<const base::DictionaryValue*>(node);
155      base::DictionaryValue* copy = new base::DictionaryValue;
156      for (base::DictionaryValue::Iterator it(*dict);
157           !it.IsAtEnd();
158           it.Advance()) {
159        base::Value* child_copy = DeepCopyAndResolveStrings(&it.value(),
160                                                            strings);
161        copy->SetWithoutPathExpansion(it.key(), child_copy);
162      }
163      return copy;
164    }
165
166    case base::Value::TYPE_STRING: {
167      std::string value;
168      bool rv = node->GetAsString(&value);
169      DCHECK(rv);
170      std::string actual_value;
171      if (!strings || !strings->GetString(value, &actual_value))
172        actual_value = value;
173      return new base::StringValue(actual_value);
174    }
175
176    default:
177      // For everything else, just make a copy.
178      return node->DeepCopy();
179  }
180}
181
182void AppendQueryParameter(GURL* url,
183                          const std::string& param,
184                          const std::string& value) {
185  *url = net::AppendQueryParameter(*url, param, value);
186}
187
188}  // namespace
189
190NotificationPromo::NotificationPromo()
191    : prefs_(g_browser_process->local_state()),
192      promo_type_(NO_PROMO),
193      promo_payload_(new base::DictionaryValue()),
194      start_(0.0),
195      end_(0.0),
196      num_groups_(kDefaultGroupSize),
197      initial_segment_(0),
198      increment_(1),
199      time_slice_(0),
200      max_group_(0),
201      max_views_(0),
202      group_(0),
203      views_(0),
204      closed_(false),
205      new_notification_(false) {
206  DCHECK(prefs_);
207}
208
209NotificationPromo::~NotificationPromo() {}
210
211void NotificationPromo::InitFromJson(const base::DictionaryValue& json,
212                                     PromoType promo_type) {
213  promo_type_ = promo_type;
214  const base::ListValue* promo_list = NULL;
215  DVLOG(1) << "InitFromJson " << PromoTypeToString(promo_type_);
216  if (!json.GetList(PromoTypeToString(promo_type_), &promo_list))
217    return;
218
219  // No support for multiple promos yet. Only consider the first one.
220  const base::DictionaryValue* promo = NULL;
221  if (!promo_list->GetDictionary(0, &promo))
222    return;
223
224  // Date.
225  const base::ListValue* date_list = NULL;
226  if (promo->GetList("date", &date_list)) {
227    const base::DictionaryValue* date;
228    if (date_list->GetDictionary(0, &date)) {
229      std::string time_str;
230      base::Time time;
231      if (date->GetString("start", &time_str) &&
232          base::Time::FromString(time_str.c_str(), &time)) {
233        start_ = time.ToDoubleT();
234        DVLOG(1) << "start str=" << time_str
235                 << ", start_="<< base::DoubleToString(start_);
236      }
237      if (date->GetString("end", &time_str) &&
238          base::Time::FromString(time_str.c_str(), &time)) {
239        end_ = time.ToDoubleT();
240        DVLOG(1) << "end str =" << time_str
241                 << ", end_=" << base::DoubleToString(end_);
242      }
243    }
244  }
245
246  // Grouping.
247  const base::DictionaryValue* grouping = NULL;
248  if (promo->GetDictionary("grouping", &grouping)) {
249    grouping->GetInteger("buckets", &num_groups_);
250    grouping->GetInteger("segment", &initial_segment_);
251    grouping->GetInteger("increment", &increment_);
252    grouping->GetInteger("increment_frequency", &time_slice_);
253    grouping->GetInteger("increment_max", &max_group_);
254
255    DVLOG(1) << "num_groups_ = " << num_groups_
256             << ", initial_segment_ = " << initial_segment_
257             << ", increment_ = " << increment_
258             << ", time_slice_ = " << time_slice_
259             << ", max_group_ = " << max_group_;
260  }
261
262  // Strings.
263  const base::DictionaryValue* strings = NULL;
264  promo->GetDictionary("strings", &strings);
265
266  // Payload.
267  const base::DictionaryValue* payload = NULL;
268  if (promo->GetDictionary("payload", &payload)) {
269    base::Value* ppcopy = DeepCopyAndResolveStrings(payload, strings);
270    DCHECK(ppcopy && ppcopy->IsType(base::Value::TYPE_DICTIONARY));
271    promo_payload_.reset(static_cast<base::DictionaryValue*>(ppcopy));
272  }
273
274  if (!promo_payload_->GetString("promo_message_short", &promo_text_) &&
275      strings) {
276    // For compatibility with the legacy desktop version,
277    // if no |payload.promo_message_short| is specified,
278    // the first string in |strings| is used.
279    base::DictionaryValue::Iterator iter(*strings);
280    iter.value().GetAsString(&promo_text_);
281  }
282  DVLOG(1) << "promo_text_=" << promo_text_;
283
284  promo->GetInteger("max_views", &max_views_);
285  DVLOG(1) << "max_views_ " << max_views_;
286
287  CheckForNewNotification();
288}
289
290void NotificationPromo::CheckForNewNotification() {
291  NotificationPromo old_promo;
292  old_promo.InitFromPrefs(promo_type_);
293  const double old_start = old_promo.start_;
294  const double old_end = old_promo.end_;
295  const std::string old_promo_text = old_promo.promo_text_;
296
297  new_notification_ =
298      old_start != start_ || old_end != end_ || old_promo_text != promo_text_;
299  if (new_notification_)
300    OnNewNotification();
301}
302
303void NotificationPromo::OnNewNotification() {
304  DVLOG(1) << "OnNewNotification";
305  // Create a new promo group.
306  group_ = base::RandInt(0, num_groups_ - 1);
307  WritePrefs();
308}
309
310// static
311void NotificationPromo::RegisterPrefs(PrefRegistrySimple* registry) {
312  registry->RegisterDictionaryPref(kPrefPromoObject);
313}
314
315// static
316void NotificationPromo::RegisterProfilePrefs(
317    user_prefs::PrefRegistrySyncable* registry) {
318  // TODO(dbeam): Registered only for migration. Remove in M28 when
319  // we're reasonably sure all prefs are gone.
320  // http://crbug.com/168887
321  registry->RegisterDictionaryPref(
322      kPrefPromoObject, user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF);
323}
324
325// static
326void NotificationPromo::MigrateUserPrefs(PrefService* user_prefs) {
327  user_prefs->ClearPref(kPrefPromoObject);
328}
329
330void NotificationPromo::WritePrefs() {
331  base::DictionaryValue* ntp_promo = new base::DictionaryValue;
332  ntp_promo->SetString(kPrefPromoText, promo_text_);
333  ntp_promo->Set(kPrefPromoPayload, promo_payload_->DeepCopy());
334  ntp_promo->SetDouble(kPrefPromoStart, start_);
335  ntp_promo->SetDouble(kPrefPromoEnd, end_);
336
337  ntp_promo->SetInteger(kPrefPromoNumGroups, num_groups_);
338  ntp_promo->SetInteger(kPrefPromoSegment, initial_segment_);
339  ntp_promo->SetInteger(kPrefPromoIncrement, increment_);
340  ntp_promo->SetInteger(kPrefPromoIncrementFrequency, time_slice_);
341  ntp_promo->SetInteger(kPrefPromoIncrementMax, max_group_);
342
343  ntp_promo->SetInteger(kPrefPromoMaxViews, max_views_);
344
345  ntp_promo->SetInteger(kPrefPromoGroup, group_);
346  ntp_promo->SetInteger(kPrefPromoViews, views_);
347  ntp_promo->SetBoolean(kPrefPromoClosed, closed_);
348
349  base::ListValue* promo_list = new base::ListValue;
350  promo_list->Set(0, ntp_promo);  // Only support 1 promo for now.
351
352  base::DictionaryValue promo_dict;
353  promo_dict.MergeDictionary(prefs_->GetDictionary(kPrefPromoObject));
354  promo_dict.Set(PromoTypeToString(promo_type_), promo_list);
355  prefs_->Set(kPrefPromoObject, promo_dict);
356  DVLOG(1) << "WritePrefs " << promo_dict;
357}
358
359void NotificationPromo::InitFromPrefs(PromoType promo_type) {
360  promo_type_ = promo_type;
361  const base::DictionaryValue* promo_dict =
362      prefs_->GetDictionary(kPrefPromoObject);
363  if (!promo_dict)
364    return;
365
366  const base::ListValue* promo_list = NULL;
367  promo_dict->GetList(PromoTypeToString(promo_type_), &promo_list);
368  if (!promo_list)
369    return;
370
371  const base::DictionaryValue* ntp_promo = NULL;
372  promo_list->GetDictionary(0, &ntp_promo);
373  if (!ntp_promo)
374    return;
375
376  ntp_promo->GetString(kPrefPromoText, &promo_text_);
377  const base::DictionaryValue* promo_payload = NULL;
378  if (ntp_promo->GetDictionary(kPrefPromoPayload, &promo_payload))
379    promo_payload_.reset(promo_payload->DeepCopy());
380
381  ntp_promo->GetDouble(kPrefPromoStart, &start_);
382  ntp_promo->GetDouble(kPrefPromoEnd, &end_);
383
384  ntp_promo->GetInteger(kPrefPromoNumGroups, &num_groups_);
385  ntp_promo->GetInteger(kPrefPromoSegment, &initial_segment_);
386  ntp_promo->GetInteger(kPrefPromoIncrement, &increment_);
387  ntp_promo->GetInteger(kPrefPromoIncrementFrequency, &time_slice_);
388  ntp_promo->GetInteger(kPrefPromoIncrementMax, &max_group_);
389
390  ntp_promo->GetInteger(kPrefPromoMaxViews, &max_views_);
391
392  ntp_promo->GetInteger(kPrefPromoGroup, &group_);
393  ntp_promo->GetInteger(kPrefPromoViews, &views_);
394  ntp_promo->GetBoolean(kPrefPromoClosed, &closed_);
395}
396
397bool NotificationPromo::CheckAppLauncher() const {
398#if !defined(ENABLE_APP_LIST)
399  return true;
400#else
401  bool is_app_launcher_promo = false;
402  if (!promo_payload_->GetBoolean("is_app_launcher_promo",
403                                  &is_app_launcher_promo))
404    return true;
405  return !is_app_launcher_promo ||
406         !prefs_->GetBoolean(prefs::kAppLauncherIsEnabled);
407#endif  // !defined(ENABLE_APP_LIST)
408}
409
410bool NotificationPromo::CanShow() const {
411  return !closed_ &&
412         !promo_text_.empty() &&
413         !ExceedsMaxGroup() &&
414         !ExceedsMaxViews() &&
415         CheckAppLauncher() &&
416         base::Time::FromDoubleT(StartTimeForGroup()) < base::Time::Now() &&
417         base::Time::FromDoubleT(EndTime()) > base::Time::Now();
418}
419
420// static
421void NotificationPromo::HandleClosed(PromoType promo_type) {
422  content::RecordAction(UserMetricsAction("NTPPromoClosed"));
423  NotificationPromo promo;
424  promo.InitFromPrefs(promo_type);
425  if (!promo.closed_) {
426    promo.closed_ = true;
427    promo.WritePrefs();
428  }
429}
430
431// static
432bool NotificationPromo::HandleViewed(PromoType promo_type) {
433  content::RecordAction(UserMetricsAction("NTPPromoShown"));
434  NotificationPromo promo;
435  promo.InitFromPrefs(promo_type);
436  ++promo.views_;
437  promo.WritePrefs();
438  return promo.ExceedsMaxViews();
439}
440
441bool NotificationPromo::ExceedsMaxGroup() const {
442  return (max_group_ == 0) ? false : group_ >= max_group_;
443}
444
445bool NotificationPromo::ExceedsMaxViews() const {
446  return (max_views_ == 0) ? false : views_ >= max_views_;
447}
448
449// static
450GURL NotificationPromo::PromoServerURL() {
451  GURL url(promo_server_url);
452  AppendQueryParameter(&url, "dist", ChannelString());
453  AppendQueryParameter(&url, "osname", PlatformString());
454  AppendQueryParameter(&url, "branding", chrome::VersionInfo().Version());
455  AppendQueryParameter(&url, "osver", base::SysInfo::OperatingSystemVersion());
456  DVLOG(1) << "PromoServerURL=" << url.spec();
457  // Note that locale param is added by WebResourceService.
458  return url;
459}
460
461double NotificationPromo::StartTimeForGroup() const {
462  if (group_ < initial_segment_)
463    return start_;
464  return start_ +
465      std::ceil(static_cast<float>(group_ - initial_segment_ + 1) / increment_)
466      * time_slice_;
467}
468
469double NotificationPromo::EndTime() const {
470  return end_;
471}
472