1// Copyright 2014 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/notifications/extension_welcome_notification.h"
6
7#include "base/guid.h"
8#include "base/lazy_instance.h"
9#include "base/message_loop/message_loop.h"
10#include "base/prefs/pref_service.h"
11#include "base/strings/utf_string_conversions.h"
12#include "chrome/browser/browser_process.h"
13#include "chrome/browser/notifications/notification.h"
14#include "chrome/browser/prefs/pref_service_syncable.h"
15#include "chrome/browser/profiles/profile.h"
16#include "chrome/browser/ui/browser_navigator.h"
17#include "chrome/common/pref_names.h"
18#include "chrome/common/url_constants.h"
19#include "chrome/grit/generated_resources.h"
20#include "components/pref_registry/pref_registry_syncable.h"
21#include "grit/theme_resources.h"
22#include "ui/base/l10n/l10n_util.h"
23#include "ui/base/resource/resource_bundle.h"
24#include "ui/message_center/message_center.h"
25#include "ui/message_center/notification.h"
26#include "ui/message_center/notification_delegate.h"
27#include "ui/message_center/notification_types.h"
28
29const int ExtensionWelcomeNotification::kRequestedShowTimeDays = 14;
30const char ExtensionWelcomeNotification::kChromeNowExtensionID[] =
31    "pafkbggdmjlpgkdkcbjmhmfcdpncadgh";
32
33namespace {
34
35class NotificationCallbacks
36    : public message_center::NotificationDelegate {
37 public:
38  NotificationCallbacks(
39      Profile* profile,
40      const message_center::NotifierId notifier_id,
41      const std::string& welcome_notification_id,
42      ExtensionWelcomeNotification::Delegate* delegate)
43      : profile_(profile),
44        notifier_id_(notifier_id.type, notifier_id.id),
45        welcome_notification_id_(welcome_notification_id),
46        delegate_(delegate) {
47  }
48
49  // Overridden from NotificationDelegate:
50  virtual void Display() OVERRIDE {}
51  virtual void Error() OVERRIDE {}
52
53  virtual void Close(bool by_user) OVERRIDE {
54    if (by_user) {
55      // Setting the preference here may cause the notification erasing
56      // to reenter. Posting a task avoids this issue.
57      delegate_->PostTask(
58          FROM_HERE,
59          base::Bind(&NotificationCallbacks::MarkAsDismissed, this));
60    }
61  }
62
63  virtual void Click() OVERRIDE {}
64  virtual void ButtonClick(int index) OVERRIDE {
65    if (index == 0) {
66      OpenNotificationLearnMoreTab();
67    } else if (index == 1) {
68      DisableNotificationProvider();
69      Close(true);
70    } else {
71      NOTREACHED();
72    }
73  }
74
75 private:
76  void MarkAsDismissed() {
77    profile_->GetPrefs()->SetBoolean(prefs::kWelcomeNotificationDismissedLocal,
78                                     true);
79  }
80
81  void OpenNotificationLearnMoreTab() {
82    chrome::NavigateParams params(
83        profile_,
84        GURL(chrome::kNotificationWelcomeLearnMoreURL),
85        ui::PAGE_TRANSITION_LINK);
86    params.disposition = NEW_FOREGROUND_TAB;
87    params.window_action = chrome::NavigateParams::SHOW_WINDOW;
88    chrome::Navigate(&params);
89  }
90
91  void DisableNotificationProvider() {
92    message_center::Notifier notifier(notifier_id_, base::string16(), true);
93    message_center::MessageCenter* message_center =
94        delegate_->GetMessageCenter();
95    message_center->DisableNotificationsByNotifier(notifier_id_);
96    message_center->RemoveNotification(welcome_notification_id_, false);
97    message_center->GetNotifierSettingsProvider()->SetNotifierEnabled(
98        notifier, false);
99  }
100
101  virtual ~NotificationCallbacks() {}
102
103  Profile* const profile_;
104
105  const message_center::NotifierId notifier_id_;
106
107  std::string welcome_notification_id_;
108
109  // Weak ref owned by ExtensionWelcomeNotification.
110  ExtensionWelcomeNotification::Delegate* const delegate_;
111
112  DISALLOW_COPY_AND_ASSIGN(NotificationCallbacks);
113};
114
115class DefaultDelegate : public ExtensionWelcomeNotification::Delegate {
116 public:
117  DefaultDelegate() {}
118
119  virtual message_center::MessageCenter* GetMessageCenter() OVERRIDE {
120    return g_browser_process->message_center();
121  }
122
123  virtual base::Time GetCurrentTime() OVERRIDE {
124    return base::Time::Now();
125  }
126
127  virtual void PostTask(
128      const tracked_objects::Location& from_here,
129      const base::Closure& task) OVERRIDE {
130    base::MessageLoop::current()->PostTask(from_here, task);
131  }
132
133 private:
134  DISALLOW_COPY_AND_ASSIGN(DefaultDelegate);
135};
136
137}  // namespace
138
139ExtensionWelcomeNotification::ExtensionWelcomeNotification(
140    Profile* const profile,
141    ExtensionWelcomeNotification::Delegate* const delegate)
142    : notifier_id_(message_center::NotifierId::APPLICATION,
143          kChromeNowExtensionID),
144      profile_(profile),
145      delegate_(delegate) {
146  welcome_notification_dismissed_pref_.Init(
147      prefs::kWelcomeNotificationDismissed,
148      profile_->GetPrefs(),
149      base::Bind(
150          &ExtensionWelcomeNotification::OnWelcomeNotificationDismissedChanged,
151          base::Unretained(this)));
152  welcome_notification_dismissed_local_pref_.Init(
153      prefs::kWelcomeNotificationDismissedLocal,
154      profile_->GetPrefs());
155}
156
157// static
158ExtensionWelcomeNotification* ExtensionWelcomeNotification::Create(
159    Profile* const profile) {
160  return Create(profile, new DefaultDelegate());
161}
162
163// static
164ExtensionWelcomeNotification* ExtensionWelcomeNotification::Create(
165    Profile* const profile, Delegate* const delegate) {
166  return new ExtensionWelcomeNotification(profile, delegate);
167}
168
169ExtensionWelcomeNotification::~ExtensionWelcomeNotification() {
170  if (delayed_notification_) {
171    delayed_notification_.reset();
172    PrefServiceSyncable::FromProfile(profile_)->RemoveObserver(this);
173  } else {
174    HideWelcomeNotification();
175  }
176}
177
178void ExtensionWelcomeNotification::OnIsSyncingChanged() {
179  DCHECK(delayed_notification_);
180  PrefServiceSyncable* const pref_service_syncable =
181      PrefServiceSyncable::FromProfile(profile_);
182  if (pref_service_syncable->IsSyncing()) {
183    pref_service_syncable->RemoveObserver(this);
184    scoped_ptr<Notification> previous_notification(
185        delayed_notification_.release());
186    ShowWelcomeNotificationIfNecessary(*(previous_notification.get()));
187  }
188}
189
190void ExtensionWelcomeNotification::ShowWelcomeNotificationIfNecessary(
191    const Notification& notification) {
192  if ((notification.notifier_id() == notifier_id_) && !delayed_notification_) {
193    PrefServiceSyncable* const pref_service_syncable =
194        PrefServiceSyncable::FromProfile(profile_);
195    if (pref_service_syncable->IsSyncing()) {
196      PrefService* const pref_service = profile_->GetPrefs();
197      if (!UserHasDismissedWelcomeNotification()) {
198        const PopUpRequest pop_up_request =
199            pref_service->GetBoolean(
200                prefs::kWelcomeNotificationPreviouslyPoppedUp)
201                ? POP_UP_HIDDEN
202                : POP_UP_SHOWN;
203        if (pop_up_request == POP_UP_SHOWN) {
204          pref_service->SetBoolean(
205              prefs::kWelcomeNotificationPreviouslyPoppedUp, true);
206        }
207
208        if (IsWelcomeNotificationExpired()) {
209          ExpireWelcomeNotification();
210        } else {
211          ShowWelcomeNotification(
212              notification.display_source(), pop_up_request);
213        }
214      }
215    } else {
216      delayed_notification_.reset(new Notification(notification));
217      pref_service_syncable->AddObserver(this);
218    }
219  }
220}
221
222// static
223void ExtensionWelcomeNotification::RegisterProfilePrefs(
224    user_prefs::PrefRegistrySyncable* prefs) {
225  prefs->RegisterBooleanPref(prefs::kWelcomeNotificationDismissed,
226                             false,
227                             user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
228  prefs->RegisterBooleanPref(prefs::kWelcomeNotificationDismissedLocal,
229                             false,
230                             user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF);
231  prefs->RegisterBooleanPref(prefs::kWelcomeNotificationPreviouslyPoppedUp,
232                             false,
233                             user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF);
234  prefs->RegisterInt64Pref(prefs::kWelcomeNotificationExpirationTimestamp,
235                           0,
236                           user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF);
237}
238
239message_center::MessageCenter*
240ExtensionWelcomeNotification::GetMessageCenter() const {
241  return delegate_->GetMessageCenter();
242}
243
244void ExtensionWelcomeNotification::ShowWelcomeNotification(
245    const base::string16& display_source,
246    const PopUpRequest pop_up_request) {
247  message_center::ButtonInfo learn_more(
248      l10n_util::GetStringUTF16(IDS_NOTIFICATION_WELCOME_BUTTON_LEARN_MORE));
249  learn_more.icon = ui::ResourceBundle::GetSharedInstance().GetImageNamed(
250      IDR_NOTIFICATION_WELCOME_LEARN_MORE);
251  message_center::ButtonInfo disable(
252      l10n_util::GetStringUTF16(IDS_NOTIFIER_WELCOME_BUTTON));
253  disable.icon = ui::ResourceBundle::GetSharedInstance().GetImageNamed(
254      IDR_NOTIFIER_BLOCK_BUTTON);
255
256  message_center::RichNotificationData rich_notification_data;
257  rich_notification_data.priority = 2;
258  rich_notification_data.buttons.push_back(learn_more);
259  rich_notification_data.buttons.push_back(disable);
260
261  if (welcome_notification_id_.empty())
262    welcome_notification_id_ = base::GenerateGUID();
263
264  if (!welcome_notification_id_.empty()) {
265    scoped_ptr<message_center::Notification> message_center_notification(
266        new message_center::Notification(
267            message_center::NOTIFICATION_TYPE_BASE_FORMAT,
268            welcome_notification_id_,
269            l10n_util::GetStringUTF16(IDS_NOTIFICATION_WELCOME_TITLE),
270            l10n_util::GetStringUTF16(IDS_NOTIFICATION_WELCOME_BODY),
271            ui::ResourceBundle::GetSharedInstance().GetImageNamed(
272                IDR_NOTIFICATION_WELCOME_ICON),
273            display_source,
274            notifier_id_,
275            rich_notification_data,
276            new NotificationCallbacks(
277                profile_, notifier_id_, welcome_notification_id_,
278                delegate_.get())));
279
280    if (pop_up_request == POP_UP_HIDDEN)
281      message_center_notification->set_shown_as_popup(true);
282
283    GetMessageCenter()->AddNotification(message_center_notification.Pass());
284    StartExpirationTimer();
285  }
286}
287
288void ExtensionWelcomeNotification::HideWelcomeNotification() {
289  if (!welcome_notification_id_.empty() &&
290      GetMessageCenter()->FindVisibleNotificationById(
291          welcome_notification_id_) != NULL) {
292    GetMessageCenter()->RemoveNotification(welcome_notification_id_, false);
293    StopExpirationTimer();
294  }
295}
296
297bool ExtensionWelcomeNotification::UserHasDismissedWelcomeNotification() const {
298  // This was previously a syncable preference; now it's per-machine.
299  // Only the local pref will be written moving forward, but check for both so
300  // users won't be double-toasted.
301  bool shown_synced = profile_->GetPrefs()->GetBoolean(
302      prefs::kWelcomeNotificationDismissed);
303  bool shown_local = profile_->GetPrefs()->GetBoolean(
304      prefs::kWelcomeNotificationDismissedLocal);
305  return (shown_synced || shown_local);
306}
307
308void ExtensionWelcomeNotification::OnWelcomeNotificationDismissedChanged() {
309  if (UserHasDismissedWelcomeNotification()) {
310    HideWelcomeNotification();
311  }
312}
313
314void ExtensionWelcomeNotification::StartExpirationTimer() {
315  if (!expiration_timer_ && !IsWelcomeNotificationExpired()) {
316    base::Time expiration_timestamp = GetExpirationTimestamp();
317    if (expiration_timestamp.is_null()) {
318      SetExpirationTimestampFromNow();
319      expiration_timestamp = GetExpirationTimestamp();
320      DCHECK(!expiration_timestamp.is_null());
321    }
322    expiration_timer_.reset(
323        new base::OneShotTimer<ExtensionWelcomeNotification>());
324    expiration_timer_->Start(
325        FROM_HERE,
326        expiration_timestamp - delegate_->GetCurrentTime(),
327        this,
328        &ExtensionWelcomeNotification::ExpireWelcomeNotification);
329  }
330}
331
332void ExtensionWelcomeNotification::StopExpirationTimer() {
333  if (expiration_timer_) {
334    expiration_timer_->Stop();
335    expiration_timer_.reset();
336  }
337}
338
339void ExtensionWelcomeNotification::ExpireWelcomeNotification() {
340  DCHECK(IsWelcomeNotificationExpired());
341  profile_->GetPrefs()->SetBoolean(
342      prefs::kWelcomeNotificationDismissedLocal, true);
343  HideWelcomeNotification();
344}
345
346base::Time ExtensionWelcomeNotification::GetExpirationTimestamp() const {
347  PrefService* const pref_service = profile_->GetPrefs();
348  const int64 expiration_timestamp =
349      pref_service->GetInt64(prefs::kWelcomeNotificationExpirationTimestamp);
350  return (expiration_timestamp == 0)
351      ? base::Time()
352      : base::Time::FromInternalValue(expiration_timestamp);
353}
354
355void ExtensionWelcomeNotification::SetExpirationTimestampFromNow() {
356  PrefService* const pref_service = profile_->GetPrefs();
357  pref_service->SetInt64(
358      prefs::kWelcomeNotificationExpirationTimestamp,
359      (delegate_->GetCurrentTime() +
360          base::TimeDelta::FromDays(kRequestedShowTimeDays)).ToInternalValue());
361}
362
363bool ExtensionWelcomeNotification::IsWelcomeNotificationExpired() const {
364  const base::Time expiration_timestamp = GetExpirationTimestamp();
365  return !expiration_timestamp.is_null() &&
366         (expiration_timestamp <= delegate_->GetCurrentTime());
367}
368