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 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 87 NotificationUIManagerMac::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 96 NotificationUIManagerMac::ControllerNotification::~ControllerNotification() { 97 [view release]; 98 delete model; 99 } 100 101 //////////////////////////////////////////////////////////////////////////////// 102 103 // static 104 NotificationUIManager* 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 127 NotificationUIManagerMac::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 134 NotificationUIManagerMac::~NotificationUIManagerMac() { 135 CancelAll(); 136 } 137 138 void 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 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 194 bool 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 202 bool 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 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 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 254 BalloonNotificationUIManager::CancelAll(); 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 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