Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 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" |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 9 | #include "base/memory/scoped_ptr.h" |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 10 | #include "base/strings/sys_string_conversions.h" |
Torne (Richard Coles) | a93a17c | 2013-05-15 11:34:50 +0100 | [diff] [blame] | 11 | #include "chrome/browser/browser_process.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 12 | #include "chrome/browser/notifications/notification.h" |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 13 | #include "chrome/browser/notifications/balloon_notification_ui_manager.h" |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 14 | #include "chrome/browser/notifications/message_center_notification_manager.h" |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 15 | #include "chrome/browser/notifications/message_center_settings_controller.h" |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 16 | #include "chrome/browser/profiles/profile.h" |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 17 | #include "chrome/browser/profiles/profile_info_cache.h" |
| 18 | #include "chrome/browser/profiles/profile_manager.h" |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 19 | #include "ui/message_center/message_center_util.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 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 | |
| 61 | namespace { |
| 62 | |
| 63 | // A "fun" way of saying: |
| 64 | // +[NSUserNotificationCenter defaultUserNotificationCenter]. |
| 65 | id<CrUserNotificationCenter> GetNotificationCenter() { |
| 66 | return [NSClassFromString(@"NSUserNotificationCenter") |
| 67 | performSelector:@selector(defaultUserNotificationCenter)]; |
| 68 | } |
| 69 | |
| 70 | // The key in NSUserNotification.userInfo that stores the C++ notification_id. |
| 71 | NSString* 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 | |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 87 | NotificationUIManagerMac::ControllerNotification::ControllerNotification( |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 88 | Profile* a_profile, |
| 89 | id<CrUserNotification> a_view, |
| 90 | Notification* a_model) |
| 91 | : profile(a_profile), |
| 92 | view(a_view), |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 93 | model(a_model) { |
| 94 | } |
| 95 | |
| 96 | NotificationUIManagerMac::ControllerNotification::~ControllerNotification() { |
| 97 | [view release]; |
| 98 | delete model; |
| 99 | } |
| 100 | |
| 101 | //////////////////////////////////////////////////////////////////////////////// |
| 102 | |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 103 | // static |
| 104 | NotificationUIManager* NotificationUIManager::Create(PrefService* local_state) { |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 105 | // TODO(rsesek): Remove this function and merge it with the one in |
| 106 | // notification_ui_manager.cc. |
| 107 | if (DelegatesToMessageCenter()) { |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 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)); |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 112 | return new MessageCenterNotificationManager( |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 113 | g_browser_process->message_center(), |
| 114 | local_state, |
| 115 | settings_provider.Pass()); |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 116 | } |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 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 | |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 127 | NotificationUIManagerMac::NotificationUIManagerMac(PrefService* local_state) |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 128 | : BalloonNotificationUIManager(local_state), |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 129 | delegate_([[NotificationCenterDelegate alloc] initWithManager:this]) { |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 130 | DCHECK(!GetNotificationCenter().delegate); |
| 131 | GetNotificationCenter().delegate = delegate_.get(); |
| 132 | } |
| 133 | |
| 134 | NotificationUIManagerMac::~NotificationUIManagerMac() { |
| 135 | CancelAll(); |
| 136 | } |
| 137 | |
| 138 | void NotificationUIManagerMac::Add(const Notification& notification, |
| 139 | Profile* profile) { |
| 140 | if (notification.is_html()) { |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 141 | BalloonNotificationUIManager::Add(notification, profile); |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 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 = |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 158 | base::SysUTF16ToNSString(notification.message()); |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 159 | ns_notification.userInfo = |
| 160 | [NSDictionary dictionaryWithObject:base::SysUTF8ToNSString( |
| 161 | notification.notification_id()) |
| 162 | forKey:kNotificationIDKey]; |
| 163 | ns_notification.hasActionButton = NO; |
| 164 | |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 165 | notification_map_.insert(std::make_pair( |
| 166 | notification.notification_id(), |
| 167 | new ControllerNotification(profile, |
| 168 | ns_notification, |
| 169 | new Notification(notification)))); |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 170 | |
| 171 | [GetNotificationCenter() deliverNotification:ns_notification]; |
| 172 | } |
| 173 | } |
| 174 | |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 175 | std::set<std::string> |
| 176 | NotificationUIManagerMac::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 | |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 194 | bool NotificationUIManagerMac::CancelById(const std::string& notification_id) { |
| 195 | NotificationMap::iterator it = notification_map_.find(notification_id); |
| 196 | if (it == notification_map_.end()) |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 197 | return BalloonNotificationUIManager::CancelById(notification_id); |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 198 | |
| 199 | return RemoveNotification(it->second->view); |
| 200 | } |
| 201 | |
| 202 | bool NotificationUIManagerMac::CancelAllBySourceOrigin( |
| 203 | const GURL& source_origin) { |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 204 | bool success = |
| 205 | BalloonNotificationUIManager::CancelAllBySourceOrigin(source_origin); |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 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 | |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 221 | bool 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 | |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 238 | void 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 | |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 254 | BalloonNotificationUIManager::CancelAll(); |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 255 | } |
| 256 | |
| 257 | const Notification* |
| 258 | NotificationUIManagerMac::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 | |
| 270 | bool 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 | |
| 300 | id<CrUserNotification> |
| 301 | NotificationUIManagerMac::FindNotificationWithReplacementId( |
| 302 | const 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 |