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 kPrefPromoMaxSeconds[] = "max_seconds";
53const char kPrefPromoFirstViewTime[] = "first_view_time";
54const char kPrefPromoGroup[] = "group";
55const char kPrefPromoViews[] = "views";
56const char kPrefPromoClosed[] = "closed";
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_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.
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    { NotificationPromo::MOBILE_NTP_WHATS_NEW_PROMO,
115        "mobile_ntp_whats_new_promo" },
116};
117
118// Convert PromoType to appropriate string.
119const 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.
138base::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
184void 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
192NotificationPromo::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
213NotificationPromo::~NotificationPromo() {}
214
215void 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
297void 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
310void 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
318void NotificationPromo::RegisterPrefs(PrefRegistrySimple* registry) {
319  registry->RegisterDictionaryPref(kPrefPromoObject);
320}
321
322// static
323void 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
333void NotificationPromo::MigrateUserPrefs(PrefService* user_prefs) {
334  user_prefs->ClearPref(kPrefPromoObject);
335}
336
337void 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
368void 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
408bool 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
421bool 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
433void 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
444bool 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
456bool NotificationPromo::ExceedsMaxGroup() const {
457  return (max_group_ == 0) ? false : group_ >= max_group_;
458}
459
460bool NotificationPromo::ExceedsMaxViews() const {
461  return (max_views_ == 0) ? false : views_ >= max_views_;
462}
463
464bool 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
474GURL 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
485double 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
493double NotificationPromo::EndTime() const {
494  return end_;
495}
496