blob: d0b783c1fdf51b9fbf47a72ce9e5dcbaff261d03 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/notifications/notification_ui_manager_mac.h"
#include "base/mac/cocoa_protocols.h"
#include "base/mac/mac_util.h"
#include "base/memory/scoped_ptr.h"
#include "base/strings/sys_string_conversions.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/notifications/notification.h"
#include "chrome/browser/notifications/balloon_notification_ui_manager.h"
#include "chrome/browser/notifications/message_center_notification_manager.h"
#include "chrome/browser/notifications/message_center_settings_controller.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_info_cache.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "ui/message_center/message_center_util.h"
@class NSUserNotificationCenter;
// Since NSUserNotification and NSUserNotificationCenter are new classes in
// 10.8, they cannot simply be declared with an @interface. An @implementation
// is needed to link, but providing one would cause a runtime conflict when
// running on 10.8. Instead, provide the interface defined as a protocol and
// use that instead, because sizeof(id<Protocol>) == sizeof(Class*). In order to
// instantiate, use NSClassFromString and simply assign the alloc/init'd result
// to an instance of the proper protocol. This way the compiler, linker, and
// loader are all happy. And the code isn't full of objc_msgSend.
@protocol CrUserNotification <NSObject>
@property(copy) NSString* title;
@property(copy) NSString* subtitle;
@property(copy) NSString* informativeText;
@property(copy) NSString* actionButtonTitle;
@property(copy) NSDictionary* userInfo;
@property(copy) NSDate* deliveryDate;
@property(copy) NSTimeZone* deliveryTimeZone;
@property(copy) NSDateComponents* deliveryRepeatInterval;
@property(readonly) NSDate* actualDeliveryDate;
@property(readonly, getter=isPresented) BOOL presented;
@property(readonly, getter=isRemote) BOOL remote;
@property(copy) NSString* soundName;
@property BOOL hasActionButton;
@end
@protocol CrUserNotificationCenter
+ (NSUserNotificationCenter*)defaultUserNotificationCenter;
@property(assign) id<NSUserNotificationCenterDelegate> delegate;
@property(copy) NSArray* scheduledNotifications;
- (void)scheduleNotification:(id<CrUserNotification>)notification;
- (void)removeScheduledNotification:(id<CrUserNotification>)notification;
@property(readonly) NSArray* deliveredNotifications;
- (void)deliverNotification:(id<CrUserNotification>)notification;
- (void)removeDeliveredNotification:(id<CrUserNotification>)notification;
- (void)removeAllDeliveredNotifications;
@end
////////////////////////////////////////////////////////////////////////////////
namespace {
// A "fun" way of saying:
// +[NSUserNotificationCenter defaultUserNotificationCenter].
id<CrUserNotificationCenter> GetNotificationCenter() {
return [NSClassFromString(@"NSUserNotificationCenter")
performSelector:@selector(defaultUserNotificationCenter)];
}
// The key in NSUserNotification.userInfo that stores the C++ notification_id.
NSString* const kNotificationIDKey = @"notification_id";
} // namespace
// A Cocoa class that can be the delegate of NSUserNotificationCenter that
// forwards commands to C++.
@interface NotificationCenterDelegate : NSObject
<NSUserNotificationCenterDelegate> {
@private
NotificationUIManagerMac* manager_; // Weak, owns self.
}
- (id)initWithManager:(NotificationUIManagerMac*)manager;
@end
////////////////////////////////////////////////////////////////////////////////
NotificationUIManagerMac::ControllerNotification::ControllerNotification(
Profile* a_profile,
id<CrUserNotification> a_view,
Notification* a_model)
: profile(a_profile),
view(a_view),
model(a_model) {
}
NotificationUIManagerMac::ControllerNotification::~ControllerNotification() {
[view release];
delete model;
}
////////////////////////////////////////////////////////////////////////////////
// static
NotificationUIManager* NotificationUIManager::Create(PrefService* local_state) {
// TODO(rsesek): Remove this function and merge it with the one in
// notification_ui_manager.cc.
if (DelegatesToMessageCenter()) {
ProfileInfoCache* profile_info_cache =
&g_browser_process->profile_manager()->GetProfileInfoCache();
scoped_ptr<message_center::NotifierSettingsProvider> settings_provider(
new MessageCenterSettingsController(profile_info_cache));
return new MessageCenterNotificationManager(
g_browser_process->message_center(),
local_state,
settings_provider.Pass());
}
BalloonNotificationUIManager* balloon_manager = NULL;
if (base::mac::IsOSMountainLionOrLater())
balloon_manager = new NotificationUIManagerMac(local_state);
else
balloon_manager = new BalloonNotificationUIManager(local_state);
balloon_manager->SetBalloonCollection(BalloonCollection::Create());
return balloon_manager;
}
NotificationUIManagerMac::NotificationUIManagerMac(PrefService* local_state)
: BalloonNotificationUIManager(local_state),
delegate_([[NotificationCenterDelegate alloc] initWithManager:this]) {
DCHECK(!GetNotificationCenter().delegate);
GetNotificationCenter().delegate = delegate_.get();
}
NotificationUIManagerMac::~NotificationUIManagerMac() {
CancelAll();
}
void NotificationUIManagerMac::Add(const Notification& notification,
Profile* profile) {
if (notification.is_html()) {
BalloonNotificationUIManager::Add(notification, profile);
} else {
if (!notification.replace_id().empty()) {
id<CrUserNotification> replacee = FindNotificationWithReplacementId(
notification.replace_id());
if (replacee)
RemoveNotification(replacee);
}
// Owned by ControllerNotification.
id<CrUserNotification> ns_notification =
[[NSClassFromString(@"NSUserNotification") alloc] init];
ns_notification.title = base::SysUTF16ToNSString(notification.title());
ns_notification.subtitle =
base::SysUTF16ToNSString(notification.display_source());
ns_notification.informativeText =
base::SysUTF16ToNSString(notification.message());
ns_notification.userInfo =
[NSDictionary dictionaryWithObject:base::SysUTF8ToNSString(
notification.notification_id())
forKey:kNotificationIDKey];
ns_notification.hasActionButton = NO;
notification_map_.insert(std::make_pair(
notification.notification_id(),
new ControllerNotification(profile,
ns_notification,
new Notification(notification))));
[GetNotificationCenter() deliverNotification:ns_notification];
}
}
std::set<std::string>
NotificationUIManagerMac::GetAllIdsByProfileAndSourceOrigin(
Profile* profile, const GURL& source_origin) {
std::set<std::string> notification_ids =
BalloonNotificationUIManager::GetAllIdsByProfileAndSourceOrigin(
profile, source_origin);
for (NotificationMap::iterator it = notification_map_.begin();
it != notification_map_.end(); ++it) {
ControllerNotification* controller_notification = it->second;
Notification* model = controller_notification->model;
if (model->origin_url() == source_origin &&
profile->IsSameProfile(controller_notification->profile)) {
notification_ids.insert(model->notification_id());
}
}
return notification_ids;
}
bool NotificationUIManagerMac::CancelById(const std::string& notification_id) {
NotificationMap::iterator it = notification_map_.find(notification_id);
if (it == notification_map_.end())
return BalloonNotificationUIManager::CancelById(notification_id);
return RemoveNotification(it->second->view);
}
bool NotificationUIManagerMac::CancelAllBySourceOrigin(
const GURL& source_origin) {
bool success =
BalloonNotificationUIManager::CancelAllBySourceOrigin(source_origin);
for (NotificationMap::iterator it = notification_map_.begin();
it != notification_map_.end();) {
if (it->second->model->origin_url() == source_origin) {
// RemoveNotification will erase from the map, invalidating iterator
// references to the removed element.
success |= RemoveNotification((it++)->second->view);
} else {
++it;
}
}
return success;
}
bool NotificationUIManagerMac::CancelAllByProfile(Profile* profile) {
bool success = BalloonNotificationUIManager::CancelAllByProfile(profile);
for (NotificationMap::iterator it = notification_map_.begin();
it != notification_map_.end();) {
if (it->second->profile == profile) {
// RemoveNotification will erase from the map, invalidating iterator
// references to the removed element.
success |= RemoveNotification((it++)->second->view);
} else {
++it;
}
}
return success;
}
void NotificationUIManagerMac::CancelAll() {
id<CrUserNotificationCenter> center = GetNotificationCenter();
// Calling RemoveNotification would loop many times over, so just replicate
// a small bit of its logic here.
for (NotificationMap::iterator it = notification_map_.begin();
it != notification_map_.end();
++it) {
it->second->model->Close(false);
delete it->second;
}
notification_map_.clear();
// Clean up any lingering ones in the system tray.
[center removeAllDeliveredNotifications];
BalloonNotificationUIManager::CancelAll();
}
const Notification*
NotificationUIManagerMac::FindNotificationWithCocoaNotification(
id<CrUserNotification> notification) const {
std::string notification_id = base::SysNSStringToUTF8(
[notification.userInfo objectForKey:kNotificationIDKey]);
NotificationMap::const_iterator it = notification_map_.find(notification_id);
if (it == notification_map_.end())
return NULL;
return it->second->model;
}
bool NotificationUIManagerMac::RemoveNotification(
id<CrUserNotification> notification) {
std::string notification_id = base::SysNSStringToUTF8(
[notification.userInfo objectForKey:kNotificationIDKey]);
id<CrUserNotificationCenter> center = GetNotificationCenter();
// First remove all Cocoa notifications from the center that match the
// notification. Notifications in the system tray do not share pointer
// equality with the balloons or any other message delievered to the
// delegate, so this loop must be run through every time to clean up stale
// notifications.
NSArray* delivered_notifications = center.deliveredNotifications;
for (id<CrUserNotification> delivered in delivered_notifications) {
if ([delivered isEqual:notification]) {
[center removeDeliveredNotification:delivered];
}
}
// Then clean up the C++ model side.
NotificationMap::iterator it = notification_map_.find(notification_id);
if (it == notification_map_.end())
return false;
it->second->model->Close(false);
delete it->second;
notification_map_.erase(it);
return true;
}
id<CrUserNotification>
NotificationUIManagerMac::FindNotificationWithReplacementId(
const string16& replacement_id) const {
for (NotificationMap::const_iterator it = notification_map_.begin();
it != notification_map_.end();
++it) {
if (it->second->model->replace_id() == replacement_id)
return it->second->view;
}
return nil;
}
////////////////////////////////////////////////////////////////////////////////
@implementation NotificationCenterDelegate
- (id)initWithManager:(NotificationUIManagerMac*)manager {
if ((self = [super init])) {
CHECK(manager);
manager_ = manager;
}
return self;
}
- (void)userNotificationCenter:(NSUserNotificationCenter*)center
didDeliverNotification:(id<CrUserNotification>)nsNotification {
const Notification* notification =
manager_->FindNotificationWithCocoaNotification(nsNotification);
if (notification)
notification->Display();
}
- (void)userNotificationCenter:(NSUserNotificationCenter*)center
didActivateNotification:(id<CrUserNotification>)nsNotification {
const Notification* notification =
manager_->FindNotificationWithCocoaNotification(nsNotification);
if (notification)
notification->Click();
}
- (BOOL)userNotificationCenter:(NSUserNotificationCenter*)center
shouldPresentNotification:(id<CrUserNotification>)nsNotification {
// Always display notifications, regardless of whether the app is foreground.
return YES;
}
@end