blob: e96756be9d07f2003944297157bc9d80f3a2cb7c [file] [log] [blame]
// Copyright 2015 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_platform_bridge_mac.h"
#include <utility>
#include "base/mac/foundation_util.h"
#include "base/mac/mac_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/sys_string_conversions.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/notifications/native_notification_display_service.h"
#include "chrome/browser/notifications/notification.h"
#include "chrome/browser/notifications/notification_common.h"
#include "chrome/browser/notifications/notification_display_service_factory.h"
#include "chrome/browser/notifications/persistent_notification_delegate.h"
#include "chrome/browser/notifications/platform_notification_service_impl.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/cocoa/notifications/notification_builder_mac.h"
#include "chrome/browser/ui/cocoa/notifications/notification_constants_mac.h"
#import "chrome/browser/ui/cocoa/notifications/notification_response_builder_mac.h"
#include "chrome/grit/generated_resources.h"
#include "components/url_formatter/elide_url.h"
#include "third_party/WebKit/public/platform/modules/notifications/WebNotificationConstants.h"
#include "ui/base/l10n/l10n_util_mac.h"
#include "url/gurl.h"
#include "url/origin.h"
@class NSUserNotification;
@class NSUserNotificationCenter;
// The mapping from web notifications to NsUserNotification works as follows
// notification#title in NSUserNotification.title
// notification#message in NSUserNotification.informativeText
// notification#context_message in NSUserNotification.subtitle
// notification#tag in NSUserNotification.identifier (10.9)
// notification#icon in NSUserNotification.contentImage (10.9)
// Site settings button is implemented as NSUserNotification's action button
// Not possible to implement:
// -notification.requireInteraction
// -The event associated to the close button
// TODO(miguelg) implement the following features
// - Sound names can be implemented by setting soundName in NSUserNotification
// NSUserNotificationDefaultSoundName gives you the platform default.
namespace {
// Callback to run once the profile has been loaded in order to perform a
// given |operation| in a notification.
void ProfileLoadedCallback(NotificationCommon::Operation operation,
NotificationCommon::Type notification_type,
const std::string& origin,
const std::string& notification_id,
int action_index,
Profile* profile) {
if (!profile) {
// TODO(miguelg): Add UMA for this condition.
// Perhaps propagate this through PersistentNotificationStatus.
LOG(WARNING) << "Profile not loaded correctly";
return;
}
NotificationDisplayService* display_service =
NotificationDisplayServiceFactory::GetForProfile(profile);
static_cast<NativeNotificationDisplayService*>(display_service)
->ProcessNotificationOperation(operation, notification_type, origin,
notification_id, action_index);
}
} // namespace
// static
NotificationPlatformBridge* NotificationPlatformBridge::Create() {
return new NotificationPlatformBridgeMac(
[NSUserNotificationCenter defaultUserNotificationCenter]);
}
// A Cocoa class that represents the delegate of NSUserNotificationCenter and
// can forward commands to C++.
@interface NotificationCenterDelegate
: NSObject<NSUserNotificationCenterDelegate> {
}
@end
// /////////////////////////////////////////////////////////////////////////////
NotificationPlatformBridgeMac::NotificationPlatformBridgeMac(
NSUserNotificationCenter* notification_center)
: delegate_([NotificationCenterDelegate alloc]),
notification_center_(notification_center) {
[notification_center_ setDelegate:delegate_.get()];
}
NotificationPlatformBridgeMac::~NotificationPlatformBridgeMac() {
[notification_center_ setDelegate:nil];
// TODO(miguelg) lift this restriction if possible.
[notification_center_ removeAllDeliveredNotifications];
}
void NotificationPlatformBridgeMac::Display(
NotificationCommon::Type notification_type,
const std::string& notification_id,
const std::string& profile_id,
bool incognito,
const Notification& notification) {
base::scoped_nsobject<NotificationBuilder> builder(
[[NotificationBuilder alloc] init]);
[builder setTitle:base::SysUTF16ToNSString(notification.title())];
[builder setContextMessage:base::SysUTF16ToNSString(notification.message())];
base::string16 subtitle =
notification.context_message().empty()
? url_formatter::FormatOriginForSecurityDisplay(
url::Origin(notification.origin_url()),
url_formatter::SchemeDisplay::OMIT_HTTP_AND_HTTPS)
: notification.context_message();
[builder setSubTitle:base::SysUTF16ToNSString(subtitle)];
if (!notification.icon().IsEmpty()) {
[builder setIcon:notification.icon().ToNSImage()];
}
std::vector<message_center::ButtonInfo> buttons = notification.buttons();
if (!buttons.empty()) {
DCHECK_LE(buttons.size(), blink::kWebNotificationMaxActions);
NSString* buttonOne = SysUTF16ToNSString(buttons[0].title);
NSString* buttonTwo = nullptr;
if (buttons.size() > 1)
buttonTwo = SysUTF16ToNSString(buttons[1].title);
[builder setButtons:buttonOne secondaryButton:buttonTwo];
}
// Tag
if (!notification.tag().empty()) {
[builder setTag:base::SysUTF8ToNSString(notification.tag())];
// If renotify is needed, delete the notification with the same tag
// from the notification center before displaying this one.
// TODO(miguelg): This will need to work for alerts as well via XPC
// once supported.
if (notification.renotify()) {
NSUserNotificationCenter* notification_center =
[NSUserNotificationCenter defaultUserNotificationCenter];
for (NSUserNotification* existing_notification in
[notification_center deliveredNotifications]) {
NSString* identifier =
[existing_notification valueForKey:@"identifier"];
if ([identifier
isEqualToString:base::SysUTF8ToNSString(notification.tag())]) {
[notification_center
removeDeliveredNotification:existing_notification];
break;
}
}
}
}
[builder setOrigin:base::SysUTF8ToNSString(notification.origin_url().spec())];
[builder setNotificationId:base::SysUTF8ToNSString(notification_id)];
[builder setProfileId:base::SysUTF8ToNSString(profile_id)];
[builder setIncognito:incognito];
[builder setNotificationType:[NSNumber numberWithInteger:notification_type]];
NSUserNotification* toast = [builder buildUserNotification];
[notification_center_ deliverNotification:toast];
}
void NotificationPlatformBridgeMac::Close(const std::string& profile_id,
const std::string& notification_id) {
NSString* candidate_id = base::SysUTF8ToNSString(notification_id);
NSString* current_profile_id = base::SysUTF8ToNSString(profile_id);
for (NSUserNotification* toast in
[notification_center_ deliveredNotifications]) {
NSString* toast_id =
[toast.userInfo objectForKey:notification_constants::kNotificationId];
NSString* persistent_profile_id = [toast.userInfo
objectForKey:notification_constants::kNotificationProfileId];
if ([toast_id isEqualToString:candidate_id] &&
[persistent_profile_id isEqualToString:current_profile_id]) {
[notification_center_ removeDeliveredNotification:toast];
}
}
}
bool NotificationPlatformBridgeMac::GetDisplayed(
const std::string& profile_id,
bool incognito,
std::set<std::string>* notifications) const {
DCHECK(notifications);
NSString* current_profile_id = base::SysUTF8ToNSString(profile_id);
for (NSUserNotification* toast in
[notification_center_ deliveredNotifications]) {
NSString* toast_profile_id = [toast.userInfo
objectForKey:notification_constants::kNotificationProfileId];
if (toast_profile_id == current_profile_id) {
notifications->insert(base::SysNSStringToUTF8([toast.userInfo
objectForKey:notification_constants::kNotificationId]));
}
}
return true;
}
bool NotificationPlatformBridgeMac::SupportsNotificationCenter() const {
return true;
}
// static
bool NotificationPlatformBridgeMac::VerifyNotificationData(
NSDictionary* response) {
if (![response
objectForKey:notification_constants::kNotificationButtonIndex] ||
![response objectForKey:notification_constants::kNotificationOperation] ||
![response objectForKey:notification_constants::kNotificationId] ||
![response objectForKey:notification_constants::kNotificationProfileId] ||
![response objectForKey:notification_constants::kNotificationIncognito] ||
![response objectForKey:notification_constants::kNotificationType]) {
LOG(ERROR) << "Missing required key";
return false;
}
NSNumber* button_index =
[response objectForKey:notification_constants::kNotificationButtonIndex];
NSNumber* operation =
[response objectForKey:notification_constants::kNotificationOperation];
NSString* notification_id =
[response objectForKey:notification_constants::kNotificationId];
NSString* profile_id =
[response objectForKey:notification_constants::kNotificationProfileId];
NSNumber* notification_type =
[response objectForKey:notification_constants::kNotificationType];
if (button_index.intValue < -1 ||
button_index.intValue >=
static_cast<int>(blink::kWebNotificationMaxActions)) {
LOG(ERROR) << "Invalid number of buttons supplied "
<< button_index.intValue;
return false;
}
if (operation.unsignedIntValue > NotificationCommon::OPERATION_MAX) {
LOG(ERROR) << operation.unsignedIntValue
<< " does not correspond to a valid operation.";
return false;
}
if (notification_id.length <= 0) {
LOG(ERROR) << "Notification Id is empty";
return false;
}
if (profile_id.length <= 0) {
LOG(ERROR) << "ProfileId not provided";
return false;
}
if (notification_type.unsignedIntValue > NotificationCommon::TYPE_MAX) {
LOG(ERROR) << notification_type.unsignedIntValue
<< " Does not correspond to a valid operation.";
return false;
}
// Origin is not actually required but if it's there it should be a valid one.
NSString* origin =
[response objectForKey:notification_constants::kNotificationOrigin];
if (origin) {
std::string notificationOrigin = base::SysNSStringToUTF8(origin);
GURL url(notificationOrigin);
if (!url.is_valid())
return false;
}
return true;
}
// /////////////////////////////////////////////////////////////////////////////
@implementation NotificationCenterDelegate
- (void)userNotificationCenter:(NSUserNotificationCenter*)center
didActivateNotification:(NSUserNotification*)notification {
NSDictionary* response =
[NotificationResponseBuilder buildDictionary:notification];
if (!NotificationPlatformBridgeMac::VerifyNotificationData(response))
return;
NSNumber* buttonIndex =
[response objectForKey:notification_constants::kNotificationButtonIndex];
NSNumber* operation =
[response objectForKey:notification_constants::kNotificationOperation];
std::string notificationOrigin = base::SysNSStringToUTF8(
[response objectForKey:notification_constants::kNotificationOrigin]);
NSString* notificationId =
[response objectForKey:notification_constants::kNotificationId];
std::string persistentNotificationId =
base::SysNSStringToUTF8(notificationId);
std::string profileId = base::SysNSStringToUTF8(
[response objectForKey:notification_constants::kNotificationProfileId]);
NSNumber* isIncognito =
[response objectForKey:notification_constants::kNotificationIncognito];
NSNumber* notificationType =
[response objectForKey:notification_constants::kNotificationType];
ProfileManager* profileManager = g_browser_process->profile_manager();
DCHECK(profileManager);
profileManager->LoadProfile(
profileId, [isIncognito boolValue],
base::Bind(
&ProfileLoadedCallback, static_cast<NotificationCommon::Operation>(
operation.unsignedIntValue),
static_cast<NotificationCommon::Type>(
notificationType.unsignedIntValue),
notificationOrigin, persistentNotificationId, buttonIndex.intValue));
}
- (BOOL)userNotificationCenter:(NSUserNotificationCenter*)center
shouldPresentNotification:(NSUserNotification*)nsNotification {
// Always display notifications, regardless of whether the app is foreground.
return YES;
}
@end