Home | History | Annotate | Download | only in notifications
      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