• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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/notifications/notification_ui_manager_mac.h"
6
7#include "base/mac/cocoa_protocols.h"
8#include "base/mac/mac_util.h"
9#include "base/memory/scoped_ptr.h"
10#include "base/strings/sys_string_conversions.h"
11#include "chrome/browser/browser_process.h"
12#include "chrome/browser/notifications/notification.h"
13#include "chrome/browser/notifications/balloon_notification_ui_manager.h"
14#include "chrome/browser/notifications/message_center_notification_manager.h"
15#include "chrome/browser/notifications/message_center_settings_controller.h"
16#include "chrome/browser/profiles/profile.h"
17#include "chrome/browser/profiles/profile_info_cache.h"
18#include "chrome/browser/profiles/profile_manager.h"
19#include "ui/message_center/message_center_util.h"
20
21@class NSUserNotificationCenter;
22
23// Since NSUserNotification and NSUserNotificationCenter are new classes in
24// 10.8, they cannot simply be declared with an @interface. An @implementation
25// is needed to link, but providing one would cause a runtime conflict when
26// running on 10.8. Instead, provide the interface defined as a protocol and
27// use that instead, because sizeof(id<Protocol>) == sizeof(Class*). In order to
28// instantiate, use NSClassFromString and simply assign the alloc/init'd result
29// to an instance of the proper protocol. This way the compiler, linker, and
30// loader are all happy. And the code isn't full of objc_msgSend.
31@protocol CrUserNotification <NSObject>
32@property(copy) NSString* title;
33@property(copy) NSString* subtitle;
34@property(copy) NSString* informativeText;
35@property(copy) NSString* actionButtonTitle;
36@property(copy) NSDictionary* userInfo;
37@property(copy) NSDate* deliveryDate;
38@property(copy) NSTimeZone* deliveryTimeZone;
39@property(copy) NSDateComponents* deliveryRepeatInterval;
40@property(readonly) NSDate* actualDeliveryDate;
41@property(readonly, getter=isPresented) BOOL presented;
42@property(readonly, getter=isRemote) BOOL remote;
43@property(copy) NSString* soundName;
44@property BOOL hasActionButton;
45@end
46
47@protocol CrUserNotificationCenter
48+ (NSUserNotificationCenter*)defaultUserNotificationCenter;
49@property(assign) id<NSUserNotificationCenterDelegate> delegate;
50@property(copy) NSArray* scheduledNotifications;
51- (void)scheduleNotification:(id<CrUserNotification>)notification;
52- (void)removeScheduledNotification:(id<CrUserNotification>)notification;
53@property(readonly) NSArray* deliveredNotifications;
54- (void)deliverNotification:(id<CrUserNotification>)notification;
55- (void)removeDeliveredNotification:(id<CrUserNotification>)notification;
56- (void)removeAllDeliveredNotifications;
57@end
58
59////////////////////////////////////////////////////////////////////////////////
60
61namespace {
62
63// A "fun" way of saying:
64//   +[NSUserNotificationCenter defaultUserNotificationCenter].
65id<CrUserNotificationCenter> GetNotificationCenter() {
66  return [NSClassFromString(@"NSUserNotificationCenter")
67      performSelector:@selector(defaultUserNotificationCenter)];
68}
69
70// The key in NSUserNotification.userInfo that stores the C++ notification_id.
71NSString* const kNotificationIDKey = @"notification_id";
72
73}  // namespace
74
75// A Cocoa class that can be the delegate of NSUserNotificationCenter that
76// forwards commands to C++.
77@interface NotificationCenterDelegate : NSObject
78    <NSUserNotificationCenterDelegate> {
79 @private
80  NotificationUIManagerMac* manager_;  // Weak, owns self.
81}
82- (id)initWithManager:(NotificationUIManagerMac*)manager;
83@end
84
85////////////////////////////////////////////////////////////////////////////////
86
87NotificationUIManagerMac::ControllerNotification::ControllerNotification(
88    Profile* a_profile,
89    id<CrUserNotification> a_view,
90    Notification* a_model)
91    : profile(a_profile),
92      view(a_view),
93      model(a_model) {
94}
95
96NotificationUIManagerMac::ControllerNotification::~ControllerNotification() {
97  [view release];
98  delete model;
99}
100
101////////////////////////////////////////////////////////////////////////////////
102
103// static
104NotificationUIManager* NotificationUIManager::Create(PrefService* local_state) {
105  // TODO(rsesek): Remove this function and merge it with the one in
106  // notification_ui_manager.cc.
107  if (DelegatesToMessageCenter()) {
108    ProfileInfoCache* profile_info_cache =
109        &g_browser_process->profile_manager()->GetProfileInfoCache();
110    scoped_ptr<message_center::NotifierSettingsProvider> settings_provider(
111        new MessageCenterSettingsController(profile_info_cache));
112    return new MessageCenterNotificationManager(
113        g_browser_process->message_center(),
114        local_state,
115        settings_provider.Pass());
116  }
117
118  BalloonNotificationUIManager* balloon_manager = NULL;
119  if (base::mac::IsOSMountainLionOrLater())
120    balloon_manager = new NotificationUIManagerMac(local_state);
121  else
122    balloon_manager = new BalloonNotificationUIManager(local_state);
123  balloon_manager->SetBalloonCollection(BalloonCollection::Create());
124  return balloon_manager;
125}
126
127NotificationUIManagerMac::NotificationUIManagerMac(PrefService* local_state)
128    : BalloonNotificationUIManager(local_state),
129      delegate_([[NotificationCenterDelegate alloc] initWithManager:this]) {
130  DCHECK(!GetNotificationCenter().delegate);
131  GetNotificationCenter().delegate = delegate_.get();
132}
133
134NotificationUIManagerMac::~NotificationUIManagerMac() {
135  CancelAll();
136}
137
138void NotificationUIManagerMac::Add(const Notification& notification,
139                                   Profile* profile) {
140  if (notification.is_html()) {
141    BalloonNotificationUIManager::Add(notification, profile);
142  } else {
143    if (!notification.replace_id().empty()) {
144      id<CrUserNotification> replacee = FindNotificationWithReplacementId(
145          notification.replace_id());
146      if (replacee)
147        RemoveNotification(replacee);
148    }
149
150    // Owned by ControllerNotification.
151    id<CrUserNotification> ns_notification =
152        [[NSClassFromString(@"NSUserNotification") alloc] init];
153
154    ns_notification.title = base::SysUTF16ToNSString(notification.title());
155    ns_notification.subtitle =
156        base::SysUTF16ToNSString(notification.display_source());
157    ns_notification.informativeText =
158        base::SysUTF16ToNSString(notification.message());
159    ns_notification.userInfo =
160        [NSDictionary dictionaryWithObject:base::SysUTF8ToNSString(
161            notification.notification_id())
162                                    forKey:kNotificationIDKey];
163    ns_notification.hasActionButton = NO;
164
165    notification_map_.insert(std::make_pair(
166        notification.notification_id(),
167        new ControllerNotification(profile,
168                                   ns_notification,
169                                   new Notification(notification))));
170
171    [GetNotificationCenter() deliverNotification:ns_notification];
172  }
173}
174
175std::set<std::string>
176NotificationUIManagerMac::GetAllIdsByProfileAndSourceOrigin(
177    Profile* profile, const GURL& source_origin) {
178  std::set<std::string> notification_ids =
179      BalloonNotificationUIManager::GetAllIdsByProfileAndSourceOrigin(
180          profile, source_origin);
181
182  for (NotificationMap::iterator it = notification_map_.begin();
183       it != notification_map_.end(); ++it) {
184    ControllerNotification* controller_notification = it->second;
185    Notification* model = controller_notification->model;
186    if (model->origin_url() == source_origin &&
187        profile->IsSameProfile(controller_notification->profile)) {
188      notification_ids.insert(model->notification_id());
189    }
190  }
191  return notification_ids;
192}
193
194bool NotificationUIManagerMac::CancelById(const std::string& notification_id) {
195  NotificationMap::iterator it = notification_map_.find(notification_id);
196  if (it == notification_map_.end())
197    return BalloonNotificationUIManager::CancelById(notification_id);
198
199  return RemoveNotification(it->second->view);
200}
201
202bool NotificationUIManagerMac::CancelAllBySourceOrigin(
203    const GURL& source_origin) {
204  bool success =
205      BalloonNotificationUIManager::CancelAllBySourceOrigin(source_origin);
206
207  for (NotificationMap::iterator it = notification_map_.begin();
208       it != notification_map_.end();) {
209    if (it->second->model->origin_url() == source_origin) {
210      // RemoveNotification will erase from the map, invalidating iterator
211      // references to the removed element.
212      success |= RemoveNotification((it++)->second->view);
213    } else {
214      ++it;
215    }
216  }
217
218  return success;
219}
220
221bool NotificationUIManagerMac::CancelAllByProfile(Profile* profile) {
222  bool success = BalloonNotificationUIManager::CancelAllByProfile(profile);
223
224  for (NotificationMap::iterator it = notification_map_.begin();
225       it != notification_map_.end();) {
226    if (it->second->profile == profile) {
227      // RemoveNotification will erase from the map, invalidating iterator
228      // references to the removed element.
229      success |= RemoveNotification((it++)->second->view);
230    } else {
231      ++it;
232    }
233  }
234
235  return success;
236}
237
238void NotificationUIManagerMac::CancelAll() {
239  id<CrUserNotificationCenter> center = GetNotificationCenter();
240
241  // Calling RemoveNotification would loop many times over, so just replicate
242  // a small bit of its logic here.
243  for (NotificationMap::iterator it = notification_map_.begin();
244       it != notification_map_.end();
245       ++it) {
246    it->second->model->Close(false);
247    delete it->second;
248  }
249  notification_map_.clear();
250
251  // Clean up any lingering ones in the system tray.
252  [center removeAllDeliveredNotifications];
253
254  BalloonNotificationUIManager::CancelAll();
255}
256
257const Notification*
258NotificationUIManagerMac::FindNotificationWithCocoaNotification(
259    id<CrUserNotification> notification) const {
260  std::string notification_id = base::SysNSStringToUTF8(
261      [notification.userInfo objectForKey:kNotificationIDKey]);
262
263  NotificationMap::const_iterator it = notification_map_.find(notification_id);
264  if (it == notification_map_.end())
265    return NULL;
266
267  return it->second->model;
268}
269
270bool NotificationUIManagerMac::RemoveNotification(
271    id<CrUserNotification> notification) {
272  std::string notification_id = base::SysNSStringToUTF8(
273      [notification.userInfo objectForKey:kNotificationIDKey]);
274  id<CrUserNotificationCenter> center = GetNotificationCenter();
275
276  // First remove all Cocoa notifications from the center that match the
277  // notification. Notifications in the system tray do not share pointer
278  // equality with the balloons or any other message delievered to the
279  // delegate, so this loop must be run through every time to clean up stale
280  // notifications.
281  NSArray* delivered_notifications = center.deliveredNotifications;
282  for (id<CrUserNotification> delivered in delivered_notifications) {
283    if ([delivered isEqual:notification]) {
284      [center removeDeliveredNotification:delivered];
285    }
286  }
287
288  // Then clean up the C++ model side.
289  NotificationMap::iterator it = notification_map_.find(notification_id);
290  if (it == notification_map_.end())
291    return false;
292
293  it->second->model->Close(false);
294  delete it->second;
295  notification_map_.erase(it);
296
297  return true;
298}
299
300id<CrUserNotification>
301NotificationUIManagerMac::FindNotificationWithReplacementId(
302    const base::string16& replacement_id) const {
303  for (NotificationMap::const_iterator it = notification_map_.begin();
304       it != notification_map_.end();
305       ++it) {
306    if (it->second->model->replace_id() == replacement_id)
307      return it->second->view;
308  }
309  return nil;
310}
311
312////////////////////////////////////////////////////////////////////////////////
313
314@implementation NotificationCenterDelegate
315
316- (id)initWithManager:(NotificationUIManagerMac*)manager {
317  if ((self = [super init])) {
318    CHECK(manager);
319    manager_ = manager;
320  }
321  return self;
322}
323
324- (void)userNotificationCenter:(NSUserNotificationCenter*)center
325        didDeliverNotification:(id<CrUserNotification>)nsNotification {
326  const Notification* notification =
327      manager_->FindNotificationWithCocoaNotification(nsNotification);
328  if (notification)
329    notification->Display();
330}
331
332- (void)userNotificationCenter:(NSUserNotificationCenter*)center
333       didActivateNotification:(id<CrUserNotification>)nsNotification {
334  const Notification* notification =
335      manager_->FindNotificationWithCocoaNotification(nsNotification);
336  if (notification)
337    notification->Click();
338}
339
340- (BOOL)userNotificationCenter:(NSUserNotificationCenter*)center
341     shouldPresentNotification:(id<CrUserNotification>)nsNotification {
342  // Always display notifications, regardless of whether the app is foreground.
343  return YES;
344}
345
346@end
347